diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
index c8cd4b051ae45aa570bf62ad79e85bdb59350e92..2a9beba3c19447bcc6dfb09671b7ddddce6edd6a 100644
--- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
@@ -13,16 +13,18 @@ import org.briarproject.android.util.ListLoadingProgressBar;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
+import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
-import org.briarproject.api.event.SubscriptionAddedEvent;
-import org.briarproject.api.event.SubscriptionRemovedEvent;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.sync.ClientId;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -46,6 +48,7 @@ implements EventListener, OnItemClickListener {
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 	@Inject private volatile EventBus eventBus;
 
 	@Override
@@ -77,13 +80,12 @@ implements EventListener, OnItemClickListener {
 					Collection<ForumContacts> available =
 							new ArrayList<ForumContacts>();
 					long now = System.currentTimeMillis();
-					for (Forum f : forumManager.getAvailableForums()) {
+					for (Forum f : forumSharingManager.getAvailableForums()) {
 						try {
-							GroupId id = f.getId();
 							Collection<Contact> c =
-									forumManager.getSubscribers(id);
+									forumSharingManager.getSharedBy(f.getId());
 							available.add(new ForumContacts(f, c));
-						} catch (NoSuchSubscriptionException e) {
+						} catch (NoSuchGroupException e) {
 							// Continue
 						}
 					}
@@ -123,35 +125,48 @@ implements EventListener, OnItemClickListener {
 	}
 
 	public void eventOccurred(Event e) {
-		if (e instanceof RemoteSubscriptionsUpdatedEvent) {
-			LOG.info("Remote subscriptions changed, reloading");
-			loadForums();
-		} else if (e instanceof SubscriptionAddedEvent) {
-			LOG.info("Subscription added, reloading");
-			loadForums();
-		} else if (e instanceof SubscriptionRemovedEvent) {
-			LOG.info("Subscription removed, reloading");
+		if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed, reloading");
 			loadForums();
+		} else if (e instanceof GroupAddedEvent) {
+			GroupAddedEvent g = (GroupAddedEvent) e;
+			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
+				LOG.info("Forum added, reloading");
+				loadForums();
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent g = (GroupRemovedEvent) e;
+			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
+				LOG.info("Forum removed, reloading");
+				loadForums();
+			}
+		} else if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			ClientId c = m.getClientId();
+			if (m.isValid() && !m.isLocal()
+					&& c.equals(forumSharingManager.getClientId())) {
+				LOG.info("Available forums updated, reloading");
+				loadForums();
+			}
 		}
 	}
 
 	public void onItemClick(AdapterView<?> parent, View view, int position,
 			long id) {
 		AvailableForumsItem item = adapter.getItem(position);
-		Collection<ContactId> visible = new ArrayList<ContactId>();
-		for (Contact c : item.getContacts()) visible.add(c.getId());
-		addSubscription(item.getForum(), visible);
+		Collection<ContactId> shared = new ArrayList<ContactId>();
+		for (Contact c : item.getContacts()) shared.add(c.getId());
+		subscribe(item.getForum(), shared);
 		String subscribed = getString(R.string.subscribed_toast);
 		Toast.makeText(this, subscribed, LENGTH_SHORT).show();
 	}
 
-	private void addSubscription(final Forum f,
-			final Collection<ContactId> visible) {
+	private void subscribe(final Forum f, final Collection<ContactId> shared) {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
-					forumManager.addForum(f);
-					forumManager.setVisibility(f.getId(), visible);
+					forumSharingManager.addForum(f);
+					forumSharingManager.setSharedWith(f.getId(), shared);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
index 8ae263061408e2be827da4f176c3e0ef32de08a6..ca0b2ab13c8d3f0777937a2c293be8d2d5cf632e 100644
--- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
@@ -18,7 +18,7 @@ import org.briarproject.android.BriarActivity;
 import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.util.StringUtils;
 
 import java.util.logging.Logger;
@@ -51,7 +51,7 @@ implements OnEditorActionListener, OnClickListener {
 	private TextView feedback = null;
 
 	// Fields that are accessed from background threads must be volatile
-	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -138,8 +138,8 @@ implements OnEditorActionListener, OnClickListener {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					Forum f = forumManager.createForum(name);
-					forumManager.addForum(f);
+					Forum f = forumSharingManager.createForum(name);
+					forumSharingManager.addForum(f);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Storing forum took " + duration + " ms");
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index c15d2667d9004d934987c8a10d1a1fbfb422592f..943db626978ece7bc94611220c5282c40d15a8ca 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -19,13 +19,13 @@ import org.briarproject.android.util.HorizontalBorder;
 import org.briarproject.android.util.ListLoadingProgressBar;
 import org.briarproject.api.android.AndroidNotificationManager;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
-import org.briarproject.api.event.SubscriptionRemovedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostHeader;
@@ -159,7 +159,7 @@ public class ForumActivity extends BriarActivity implements EventListener,
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading forum " + duration + " ms");
 					displayForumName();
-				} catch (NoSuchSubscriptionException e) {
+				} catch (NoSuchGroupException e) {
 					finishOnUiThread();
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -188,7 +188,7 @@ public class ForumActivity extends BriarActivity implements EventListener,
 					if (LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
 					displayHeaders(headers);
-				} catch (NoSuchSubscriptionException e) {
+				} catch (NoSuchGroupException e) {
 					finishOnUiThread();
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -319,10 +319,10 @@ public class ForumActivity extends BriarActivity implements EventListener,
 				LOG.info("Message added, reloading");
 				loadHeaders();
 			}
-		} else if (e instanceof SubscriptionRemovedEvent) {
-			SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent s = (GroupRemovedEvent) e;
 			if (s.getGroup().getId().equals(groupId)) {
-				LOG.info("Subscription removed");
+				LOG.info("Forum removed");
 				finishOnUiThread();
 			}
 		}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index 55f34f75693021528fa1027914f591f6907489c6..30b62b2827f109b1307b3eb63c1d01a71c61f8f3 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -24,17 +24,17 @@ import org.briarproject.android.util.HorizontalBorder;
 import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.android.util.ListLoadingProgressBar;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
+import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
-import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
-import org.briarproject.api.event.SubscriptionAddedEvent;
-import org.briarproject.api.event.SubscriptionRemovedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.sync.ClientId;
-import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.Collection;
@@ -83,8 +83,8 @@ public class ForumListFragment extends BaseEventFragment implements
 	private ImageButton newForumButton = null;
 
 	// Fields that are accessed from background threads must be volatile
-	@Inject
-	private volatile ForumManager forumManager;
+	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 
 	@Nullable
 	@Override
@@ -164,19 +164,23 @@ public class ForumListFragment extends BaseEventFragment implements
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
+					boolean displayedHeaders = false;
 					for (Forum f : forumManager.getForums()) {
 						try {
 							Collection<ForumPostHeader> headers =
 									forumManager.getPostHeaders(f.getId());
 							displayHeaders(f, headers);
-						} catch (NoSuchSubscriptionException e) {
+							displayedHeaders = true;
+						} catch (NoSuchGroupException e) {
 							// Continue
 						}
 					}
-					int available = forumManager.getAvailableForums().size();
+					int available =
+							forumSharingManager.getAvailableForums().size();
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Full load took " + duration + " ms");
+					if (!displayedHeaders) displayEmpty();
 					displayAvailable(available);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -215,11 +219,18 @@ public class ForumListFragment extends BaseEventFragment implements
 		});
 	}
 
-	private void displayAvailable(final int availableCount) {
+	private void displayEmpty() {
 		listener.runOnUiThread(new Runnable() {
 			public void run() {
-				if (adapter.isEmpty()) empty.setVisibility(VISIBLE);
+				empty.setVisibility(VISIBLE);
 				loading.setVisibility(GONE);
+			}
+		});
+	}
+
+	private void displayAvailable(final int availableCount) {
+		listener.runOnUiThread(new Runnable() {
+			public void run() {
 				if (availableCount == 0) {
 					available.setVisibility(GONE);
 				} else {
@@ -254,25 +265,34 @@ public class ForumListFragment extends BaseEventFragment implements
 	}
 
 	public void eventOccurred(Event e) {
-		if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			ClientId c = m.getClientId();
-			if (m.isValid() && c.equals(forumManager.getClientId())) {
-				LOG.info("Message added, reloading");
-				loadHeaders(m.getMessage().getGroupId());
-			}
-		} else if (e instanceof RemoteSubscriptionsUpdatedEvent) {
-			LOG.info("Remote subscriptions changed, reloading");
+		if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed, reloading");
 			loadAvailable();
-		} else if (e instanceof SubscriptionAddedEvent) {
-			LOG.info("Group added, reloading");
-			loadHeaders();
-		} else if (e instanceof SubscriptionRemovedEvent) {
-			Group g = ((SubscriptionRemovedEvent) e).getGroup();
-			if (g.getClientId().equals(forumManager.getClientId())) {
-				LOG.info("Group removed, reloading");
+		} else if (e instanceof GroupAddedEvent) {
+			GroupAddedEvent g = (GroupAddedEvent) e;
+			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
+				LOG.info("Forum added, reloading");
+				loadHeaders();
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent g = (GroupRemovedEvent) e;
+			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
+				LOG.info("Forum removed, reloading");
 				loadHeaders();
 			}
+		} else if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			if (m.isValid()) {
+				ClientId c = m.getClientId();
+				if (c.equals(forumManager.getClientId())) {
+					LOG.info("Forum post added, reloading");
+					loadHeaders(m.getMessage().getGroupId());
+				} else if (!m.isLocal()
+						&& c.equals(forumSharingManager.getClientId())) {
+					LOG.info("Available forums updated, reloading");
+					loadAvailable();
+				}
+			}
 		}
 	}
 
@@ -288,7 +308,7 @@ public class ForumListFragment extends BaseEventFragment implements
 					if (LOG.isLoggable(INFO))
 						LOG.info("Partial load took " + duration + " ms");
 					displayHeaders(f, headers);
-				} catch (NoSuchSubscriptionException e) {
+				} catch (NoSuchGroupException e) {
 					removeForum(g);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -320,7 +340,8 @@ public class ForumListFragment extends BaseEventFragment implements
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					int available = forumManager.getAvailableForums().size();
+					int available =
+							forumSharingManager.getAvailableForums().size();
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading available took " + duration + " ms");
@@ -364,22 +385,22 @@ public class ForumListFragment extends BaseEventFragment implements
 			ContextMenuInfo info = menuItem.getMenuInfo();
 			int position = ((AdapterContextMenuInfo) info).position;
 			ForumListItem item = adapter.getItem(position);
-			removeSubscription(item.getForum());
+			unsubscribe(item.getForum());
 			String unsubscribed = getString(R.string.unsubscribed_toast);
 			Toast.makeText(getContext(), unsubscribed, LENGTH_SHORT).show();
 		}
 		return true;
 	}
 
-	private void removeSubscription(final Forum f) {
+	private void unsubscribe(final Forum f) {
 		listener.runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					forumManager.removeForum(f);
+					forumSharingManager.removeForum(f);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
-						LOG.info("Removing group took " + duration + " ms");
+						LOG.info("Removing forum took " + duration + " ms");
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
index 2b95559df059930f6c812a2a121f7e8ce7a7762b..2d237711938c893aa0e12040503e9f451e73620b 100644
--- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
@@ -19,7 +19,7 @@ import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.Collection;
@@ -52,7 +52,7 @@ SelectContactsDialog.Listener {
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile ContactManager contactManager;
-	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 	private volatile GroupId groupId = null;
 	private volatile Collection<Contact> contacts = null;
 	private volatile Collection<ContactId> selected = null;
@@ -139,7 +139,7 @@ SelectContactsDialog.Listener {
 				try {
 					long now = System.currentTimeMillis();
 					contacts = contactManager.getContacts();
-					selected = forumManager.getVisibility(groupId);
+					selected = forumSharingManager.getSharedWith(groupId);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
@@ -175,8 +175,9 @@ SelectContactsDialog.Listener {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					forumManager.setVisibleToAll(groupId, all);
-					if (!all) forumManager.setVisibility(groupId, selected);
+					if (all) forumSharingManager.setSharedWithAll(groupId);
+					else forumSharingManager.setSharedWith(groupId,
+							selected);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Update took " + duration + " ms");
diff --git a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
index 2016396234633cbc8ccd114411aed646023a0286..7b6d598a4e4133809084a6b504012a96052a7d84 100644
--- a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
+++ b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
@@ -1,27 +1,27 @@
 package org.briarproject.plugins;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.Executor;
+import android.app.Application;
+import android.content.Context;
+
+import com.google.inject.Provides;
 
 import org.briarproject.api.android.AndroidExecutor;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.event.EventBus;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
 import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
 import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
 import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
 import org.briarproject.api.system.LocationUtils;
-import org.briarproject.api.event.EventBus;
 import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
 import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory;
 import org.briarproject.plugins.tor.TorPluginFactory;
 
-import android.app.Application;
-import android.content.Context;
-
-import com.google.inject.Provides;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executor;
 
 public class AndroidPluginsModule extends PluginsModule {
 
@@ -37,11 +37,11 @@ public class AndroidPluginsModule extends PluginsModule {
 	@Provides
 	DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
 			AndroidExecutor androidExecutor, Application app,
-			CryptoComponent crypto, LocationUtils locationUtils,
+			SecureRandom random, LocationUtils locationUtils,
 			EventBus eventBus) {
 		Context appContext = app.getApplicationContext();
 		DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor,
-				androidExecutor, appContext, crypto.getSecureRandom());
+				androidExecutor, appContext, random);
 		DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext,
 				locationUtils, eventBus);
 		DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 8f65c0d4874e1e31134406a959be45863d049ce5..465a886bd7d4b2f8c8189d0bafdeb4068e215b9c 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -17,8 +17,6 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.transport.TransportKeys;
 
 import java.io.IOException;
@@ -47,14 +45,8 @@ public interface DatabaseComponent {
 	 */
 	ContactId addContact(Author remote, AuthorId local) throws DbException;
 
-	/** Adds a group to the given contact's subscriptions. */
-	void addContactGroup(ContactId c, Group g) throws DbException;
-
-	/**
-	 * Subscribes to a group, or returns false if the user already has the
-	 * maximum number of subscriptions.
-	 */
-	boolean addGroup(Group g) throws DbException;
+	/** Stores a group. */
+	void addGroup(Group g) throws DbException;
 
 	/** Stores a local pseudonym. */
 	void addLocalAuthor(LocalAuthor a) throws DbException;
@@ -63,14 +55,11 @@ public interface DatabaseComponent {
 	void addLocalMessage(Message m, ClientId c, Metadata meta, boolean shared)
 			throws DbException;
 
-	/**
-	 * Stores a transport and returns true if the transport was not previously
-	 * in the database.
-	 */
-	boolean addTransport(TransportId t, int maxLatency) throws DbException;
+	/** Stores a transport. */
+	void addTransport(TransportId t, int maxLatency) throws DbException;
 
 	/**
-	 * Stores the given transport keys for a newly added contact.
+	 * Stores transport keys for a newly added contact.
 	 */
 	void addTransportKeys(ContactId c, TransportKeys k) throws DbException;
 
@@ -113,26 +102,6 @@ public interface DatabaseComponent {
 	Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
 			int maxLatency) throws DbException;
 
-	/**
-	 * Returns a subscription ack for the given contact, or null if no
-	 * subscription ack is due.
-	 */
-	SubscriptionAck generateSubscriptionAck(ContactId c) throws DbException;
-
-	/**
-	 * Returns a subscription update for the given contact, for transmission
-	 * over a transport with the given latency. Returns null if no update is
-	 * due.
-	 */
-	SubscriptionUpdate generateSubscriptionUpdate(ContactId c, int maxLatency)
-			throws DbException;
-
-	/**
-	 * Returns all groups belonging to the given client to which the user could
-	 * subscribe.
-	 */
-	Collection<Group> getAvailableGroups(ClientId c) throws DbException;
-
 	/** Returns the contact with the given ID. */
 	Contact getContact(ContactId c) throws DbException;
 
@@ -145,16 +114,13 @@ public interface DatabaseComponent {
 	/** Returns the unique ID for this device. */
 	DeviceId getDeviceId() throws DbException;
 
-	/** Returns the group with the given ID, if the user subscribes to it. */
+	/** Returns the group with the given ID. */
 	Group getGroup(GroupId g) throws DbException;
 
 	/** Returns the metadata for the given group. */
 	Metadata getGroupMetadata(GroupId g) throws DbException;
 
-	/**
-	 * Returns all groups belonging to the given client to which the user
-	 * subscribes.
-	 */
+	/** Returns all groups belonging to the given client. */
 	Collection<Group> getGroups(ClientId c) throws DbException;
 
 	/** Returns the local pseudonym with the given ID. */
@@ -196,9 +162,6 @@ public interface DatabaseComponent {
 	/** Returns all settings in the given namespace. */
 	Settings getSettings(String namespace) throws DbException;
 
-	/** Returns all contacts who subscribe to the given group. */
-	Collection<Contact> getSubscribers(GroupId g) throws DbException;
-
 	/** Returns all transport keys for the given transport. */
 	Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
 			throws DbException;
@@ -216,6 +179,9 @@ public interface DatabaseComponent {
 	void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
 			throws DbException;
 
+	/** Returns true if the given group is visible to the given contact. */
+	boolean isVisibleToContact(ContactId c, GroupId g) throws DbException;
+
 	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
@@ -246,21 +212,10 @@ public interface DatabaseComponent {
 	/** Processes a request from the given contact. */
 	void receiveRequest(ContactId c, Request r) throws DbException;
 
-	/** Processes a subscription ack from the given contact. */
-	void receiveSubscriptionAck(ContactId c, SubscriptionAck a)
-			throws DbException;
-
-	/** Processes a subscription update from the given contact. */
-	void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u)
-			throws DbException;
-
 	/** Removes a contact (and all associated state) from the database. */
 	void removeContact(ContactId c) throws DbException;
 
-	/**
-	 * Unsubscribes from a group. Any messages belonging to the group
-	 * are deleted from the database.
-	 */
+	/** Removes a group (and all associated state) from the database. */
 	void removeGroup(Group g) throws DbException;
 
 	/**
@@ -268,10 +223,7 @@ public interface DatabaseComponent {
 	 */
 	void removeLocalAuthor(AuthorId a) throws DbException;
 
-	/**
-	 * Removes a transport (and any associated configuration and local
-	 * properties) from the database.
-	 */
+	/** Removes a transport (and all associated state) from the database. */
 	void removeTransport(TransportId t) throws DbException;
 
 	/** Sets the status of the given contact. */
@@ -297,16 +249,14 @@ public interface DatabaseComponent {
 
 	/**
 	 * Makes a group visible to the given set of contacts and invisible to any
-	 * other current or future contacts.
+	 * other contacts.
 	 */
 	void setVisibility(GroupId g, Collection<ContactId> visible)
 			throws DbException;
 
-	/**
-	 * Makes a group visible to all current and future contacts, or invisible
-	 * to future contacts.
-	 */
-	void setVisibleToAll(GroupId g, boolean all) throws DbException;
+	/** Makes a group visible or invisible to a contact. */
+	void setVisibleToContact(ContactId c, GroupId g, boolean visible)
+			throws DbException;
 
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
diff --git a/briar-api/src/org/briarproject/api/db/LocalAuthorExistsException.java b/briar-api/src/org/briarproject/api/db/LocalAuthorExistsException.java
deleted file mode 100644
index 549140364a4b2e619096002c76fe240da224a9ee..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/db/LocalAuthorExistsException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.briarproject.api.db;
-
-/**
- * Thrown when a duplicate pseudonym is added to the database. This exception
- * may occur due to concurrent updates and does not indicate a database error.
- */
-public class LocalAuthorExistsException extends DbException {
-
-	private static final long serialVersionUID = -1483877298070151673L;
-}
diff --git a/briar-api/src/org/briarproject/api/db/MessageExistsException.java b/briar-api/src/org/briarproject/api/db/MessageExistsException.java
deleted file mode 100644
index 7a3a6d2e00f5f42737bf9b78a3e72d553056feeb..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/db/MessageExistsException.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.briarproject.api.db;
-
-/**
- * Thrown when a duplicate message is added to the database. This exception may
- * occur due to concurrent updates and does not indicate a database error.
- */
-public class MessageExistsException extends DbException {
-}
diff --git a/briar-api/src/org/briarproject/api/db/NoSuchGroupException.java b/briar-api/src/org/briarproject/api/db/NoSuchGroupException.java
new file mode 100644
index 0000000000000000000000000000000000000000..17e67f5648d86fcce61275f01fdcd89a3d1b564b
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/db/NoSuchGroupException.java
@@ -0,0 +1,11 @@
+package org.briarproject.api.db;
+
+/**
+ * Thrown when a database operation is attempted for a group that is not in the
+ * database. This exception may occur due to concurrent updates and does not
+ * indicate a database error.
+ */
+public class NoSuchGroupException extends DbException {
+
+	private static final long serialVersionUID = -5494178507342571697L;
+}
diff --git a/briar-api/src/org/briarproject/api/db/NoSuchSubscriptionException.java b/briar-api/src/org/briarproject/api/db/NoSuchSubscriptionException.java
deleted file mode 100644
index fc19dc1e50570cc7e757ea24a20d5c905c043c23..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/db/NoSuchSubscriptionException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.briarproject.api.db;
-
-/**
- * Thrown when a database operation is attempted for a group to which the user
- * does not subscribe. This exception may occur due to concurrent updates and
- * does not indicate a database error.
- */
-public class NoSuchSubscriptionException extends DbException {
-
-	private static final long serialVersionUID = -5494178507342571697L;
-
-}
diff --git a/briar-api/src/org/briarproject/api/event/SubscriptionAddedEvent.java b/briar-api/src/org/briarproject/api/event/GroupAddedEvent.java
similarity index 51%
rename from briar-api/src/org/briarproject/api/event/SubscriptionAddedEvent.java
rename to briar-api/src/org/briarproject/api/event/GroupAddedEvent.java
index a9065b28b29184ac2e314414eb47e82aa22f3949..d900388bc451bb3662bd44ebc798a7735b3b656b 100644
--- a/briar-api/src/org/briarproject/api/event/SubscriptionAddedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/GroupAddedEvent.java
@@ -2,12 +2,12 @@ package org.briarproject.api.event;
 
 import org.briarproject.api.sync.Group;
 
-/** An event that is broadcast when the user subscribes to a group. */
-public class SubscriptionAddedEvent extends Event {
+/** An event that is broadcast when a group is added. */
+public class GroupAddedEvent extends Event {
 
 	private final Group group;
 
-	public SubscriptionAddedEvent(Group group) {
+	public GroupAddedEvent(Group group) {
 		this.group = group;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/SubscriptionRemovedEvent.java b/briar-api/src/org/briarproject/api/event/GroupRemovedEvent.java
similarity index 50%
rename from briar-api/src/org/briarproject/api/event/SubscriptionRemovedEvent.java
rename to briar-api/src/org/briarproject/api/event/GroupRemovedEvent.java
index a8e7524571643bd6364916f12dab5626c7e66db8..25838e610140469d870b99b48087c64c49163b59 100644
--- a/briar-api/src/org/briarproject/api/event/SubscriptionRemovedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/GroupRemovedEvent.java
@@ -2,12 +2,12 @@ package org.briarproject.api.event;
 
 import org.briarproject.api.sync.Group;
 
-/** An event that is broadcast when the user unsubscribes from a group. */
-public class SubscriptionRemovedEvent extends Event {
+/** An event that is broadcast when a group is removed. */
+public class GroupRemovedEvent extends Event {
 
 	private final Group group;
 
-	public SubscriptionRemovedEvent(Group group) {
+	public GroupRemovedEvent(Group group) {
 		this.group = group;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/LocalSubscriptionsUpdatedEvent.java b/briar-api/src/org/briarproject/api/event/GroupVisibilityUpdatedEvent.java
similarity index 56%
rename from briar-api/src/org/briarproject/api/event/LocalSubscriptionsUpdatedEvent.java
rename to briar-api/src/org/briarproject/api/event/GroupVisibilityUpdatedEvent.java
index ce7a2d909463c678319dd30b4747d6e6b213f748..368865217c487ef898e32dbfbc81ccf0e6500d86 100644
--- a/briar-api/src/org/briarproject/api/event/LocalSubscriptionsUpdatedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/GroupVisibilityUpdatedEvent.java
@@ -4,15 +4,12 @@ import org.briarproject.api.contact.ContactId;
 
 import java.util.Collection;
 
-/**
- * An event that is broadcast when the set of subscriptions visible to one or
- * more contacts is updated.
- */
-public class LocalSubscriptionsUpdatedEvent extends Event {
+/** An event that is broadcast when the visibility of a group is updated. */
+public class GroupVisibilityUpdatedEvent extends Event {
 
 	private final Collection<ContactId> affected;
 
-	public LocalSubscriptionsUpdatedEvent(Collection<ContactId> affected) {
+	public GroupVisibilityUpdatedEvent(Collection<ContactId> affected) {
 		this.affected = affected;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/event/RemoteSubscriptionsUpdatedEvent.java b/briar-api/src/org/briarproject/api/event/RemoteSubscriptionsUpdatedEvent.java
deleted file mode 100644
index cd7a17ec34e12e3d61c2aa4cc696686fc47f04bc..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/event/RemoteSubscriptionsUpdatedEvent.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.briarproject.api.event;
-
-import org.briarproject.api.contact.ContactId;
-
-/**  An event that is broadcast when a contact's subscriptions are updated. */
-public class RemoteSubscriptionsUpdatedEvent extends Event {
-
-	private final ContactId contactId;
-
-	public RemoteSubscriptionsUpdatedEvent(ContactId contactId) {
-		this.contactId = contactId;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java
index 720744a9695976c2f847be5c769ac3499899bbfd..2eeadc21664f069e32db944305a5d0756ec05561 100644
--- a/briar-api/src/org/briarproject/api/forum/Forum.java
+++ b/briar-api/src/org/briarproject/api/forum/Forum.java
@@ -7,10 +7,12 @@ public class Forum {
 
 	private final Group group;
 	private final String name;
+	private final byte[] salt;
 
-	public Forum(Group group, String name) {
+	public Forum(Group group, String name, byte[] salt) {
 		this.group = group;
 		this.name = name;
+		this.salt = salt;
 	}
 
 	public GroupId getId() {
@@ -25,6 +27,10 @@ public class Forum {
 		return name;
 	}
 
+	public byte[] getSalt() {
+		return salt;
+	}
+
 	@Override
 	public int hashCode() {
 		return group.hashCode();
diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
index 4c66fd14295a6081a0c192b331aa7da5861a05cb..035a06cd1ef800f91b1789fa3ae3f51da0c5f457 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumConstants.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
@@ -1,17 +1,16 @@
 package org.briarproject.api.forum;
 
-import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
 
 public interface ForumConstants {
 
-	/** The maximum length of a forum's name in bytes. */
-	int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10;
+	/** The maximum length of a forum's name in UTF-8 bytes. */
+	int MAX_FORUM_NAME_LENGTH = 100;
 
 	/** The length of a forum's random salt in bytes. */
 	int FORUM_SALT_LENGTH = 32;
 
-	/** The maximum length of a forum post's content type in bytes. */
+	/** The maximum length of a forum post's content type in UTF-8 bytes. */
 	int MAX_CONTENT_TYPE_LENGTH = 50;
 
 	/** The maximum length of a forum post's body in bytes. */
diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java
index f256eedd64ef56f923fe12fe71ac9574789a0abb..1d32544654c799eed46be9320f34749665d1889c 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumManager.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java
@@ -1,7 +1,5 @@
 package org.briarproject.api.forum;
 
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
@@ -14,22 +12,10 @@ public interface ForumManager {
 	/** Returns the unique ID of the forum client. */
 	ClientId getClientId();
 
-	/** Creates a forum with the given name. */
-	Forum createForum(String name);
-
-	/**
-	 * Subscribes to a forum, or returns false if the user already has the
-	 * maximum number of forum subscriptions.
-	 */
-	boolean addForum(Forum f) throws DbException;
-
 	/** Stores a local forum post. */
 	void addLocalPost(ForumPost p) throws DbException;
 
-	/** Returns all forums to which the user could subscribe. */
-	Collection<Forum> getAvailableForums() throws DbException;
-
-	/** Returns the forum with the given ID, if the user subscribes to it. */
+	/** Returns the forum with the given ID. */
 	Forum getForum(GroupId g) throws DbException;
 
 	/** Returns all forums to which the user subscribes. */
@@ -41,31 +27,6 @@ public interface ForumManager {
 	/** Returns the headers of all posts in the given forum. */
 	Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
 
-	/** Returns all contacts who subscribe to the given forum. */
-	Collection<Contact> getSubscribers(GroupId g) throws DbException;
-
-	/** Returns the IDs of all contacts to which the given forum is visible. */
-	Collection<ContactId> getVisibility(GroupId g) throws DbException;
-
-	/**
-	 * Unsubscribes from a forum. Any messages belonging to the forum are
-	 * deleted.
-	 */
-	void removeForum(Forum f) throws DbException;
-
 	/** Marks a forum post as read or unread. */
 	void setReadFlag(MessageId m, boolean read) throws DbException;
-
-	/**
-	 * Makes a forum visible to the given set of contacts and invisible to any
-	 * other current or future contacts.
-	 */
-	void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException;
-
-	/**
-	 * Makes a forum visible to all current and future contacts, or invisible
-	 * to future contacts.
-	 */
-	void setVisibleToAll(GroupId g, boolean all) throws DbException;
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..91b806e0e828d4d228c17e348ce830c5260fbc78
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
@@ -0,0 +1,43 @@
+package org.briarproject.api.forum;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+public interface ForumSharingManager {
+
+	/** Returns the unique ID of the forum sharing client. */
+	ClientId getClientId();
+
+	/** Creates a forum with the given name. */
+	Forum createForum(String name);
+
+	/** Subscribes to a forum. */
+	void addForum(Forum f) throws DbException;
+
+	/** Unsubscribes from a forum. */
+	void removeForum(Forum f) throws DbException;
+
+	/** Returns all forums to which the user could subscribe. */
+	Collection<Forum> getAvailableForums() throws DbException;
+
+	/** Returns all contacts who are sharing the given forum with the user. */
+	Collection<Contact> getSharedBy(GroupId g) throws DbException;
+
+	/** Returns the IDs of all contacts with whom the given forum is shared. */
+	Collection<ContactId> getSharedWith(GroupId g) throws DbException;
+
+	/**
+	 * Shares a forum with the given contacts and unshares it with any other
+	 * contacts.
+	 */
+	void setSharedWith(GroupId g, Collection<ContactId> shared)
+			throws DbException;
+
+	/** Shares a forum with all current and future contacts. */
+	void setSharedWithAll(GroupId g) throws DbException;
+}
diff --git a/briar-api/src/org/briarproject/api/sync/Group.java b/briar-api/src/org/briarproject/api/sync/Group.java
index 51818a3822ba93f08455d7f62ada6e6b9f880b4e..711cde85b7e04ae2b1497f59dd005bd42fb87e6c 100644
--- a/briar-api/src/org/briarproject/api/sync/Group.java
+++ b/briar-api/src/org/briarproject/api/sync/Group.java
@@ -2,7 +2,6 @@ package org.briarproject.api.sync;
 
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 
-/** A group to which users may subscribe. */
 public class Group {
 
 	private final GroupId id;
diff --git a/briar-api/src/org/briarproject/api/sync/MessageFactory.java b/briar-api/src/org/briarproject/api/sync/MessageFactory.java
index 6e65fc44cd802225abe22d44a97e037d35410c10..35a37462495619c0812536d8a5e998ea383d103e 100644
--- a/briar-api/src/org/briarproject/api/sync/MessageFactory.java
+++ b/briar-api/src/org/briarproject/api/sync/MessageFactory.java
@@ -1,9 +1,6 @@
 package org.briarproject.api.sync;
 
-import java.io.IOException;
-
 public interface MessageFactory {
 
-	Message createMessage(GroupId groupId, long timestamp, byte[] body)
-			throws IOException;
+	Message createMessage(GroupId groupId, long timestamp, byte[] body);
 }
diff --git a/briar-api/src/org/briarproject/api/sync/MessageValidator.java b/briar-api/src/org/briarproject/api/sync/MessageValidator.java
index d3d7ef520a2d0f29554f74dcb160417a28f86e19..58ee5dc2b96b980d0dea03a85a50951b0f23e4c5 100644
--- a/briar-api/src/org/briarproject/api/sync/MessageValidator.java
+++ b/briar-api/src/org/briarproject/api/sync/MessageValidator.java
@@ -8,5 +8,5 @@ public interface MessageValidator {
 	 * Validates the given message and returns its metadata if the message
 	 * is valid, or null if the message is invalid.
 	 */
-	Metadata validateMessage(Message m);
+	Metadata validateMessage(Message m, Group g);
 }
diff --git a/briar-api/src/org/briarproject/api/sync/PacketReader.java b/briar-api/src/org/briarproject/api/sync/PacketReader.java
index 8986dd81612383ed9471c17e68e8c98a584f99f4..e8893fbf9ddd687a69e179508deb64809005887d 100644
--- a/briar-api/src/org/briarproject/api/sync/PacketReader.java
+++ b/briar-api/src/org/briarproject/api/sync/PacketReader.java
@@ -17,10 +17,4 @@ public interface PacketReader {
 
 	boolean hasRequest() throws IOException;
 	Request readRequest() throws IOException;
-
-	boolean hasSubscriptionAck() throws IOException;
-	SubscriptionAck readSubscriptionAck() throws IOException;
-
-	boolean hasSubscriptionUpdate() throws IOException;
-	SubscriptionUpdate readSubscriptionUpdate() throws IOException;
 }
diff --git a/briar-api/src/org/briarproject/api/sync/PacketTypes.java b/briar-api/src/org/briarproject/api/sync/PacketTypes.java
index 2bdc1b66af9c26e7e9b987ef80ec22d8d7f4ca10..214af877a32b19839d4cad8378f56ccc13d60ddc 100644
--- a/briar-api/src/org/briarproject/api/sync/PacketTypes.java
+++ b/briar-api/src/org/briarproject/api/sync/PacketTypes.java
@@ -7,6 +7,4 @@ public interface PacketTypes {
 	byte MESSAGE = 1;
 	byte OFFER = 2;
 	byte REQUEST = 3;
-	byte SUBSCRIPTION_ACK = 6;
-	byte SUBSCRIPTION_UPDATE = 7;
 }
diff --git a/briar-api/src/org/briarproject/api/sync/PacketWriter.java b/briar-api/src/org/briarproject/api/sync/PacketWriter.java
index 7777a42943433e89e029372324f0acb34d92986f..7cc4e7436e11a6a480cbc6919719f4d56717fdeb 100644
--- a/briar-api/src/org/briarproject/api/sync/PacketWriter.java
+++ b/briar-api/src/org/briarproject/api/sync/PacketWriter.java
@@ -18,9 +18,5 @@ public interface PacketWriter {
 
 	void writeRequest(Request r) throws IOException;
 
-	void writeSubscriptionAck(SubscriptionAck a) throws IOException;
-
-	void writeSubscriptionUpdate(SubscriptionUpdate u) throws IOException;
-
 	void flush() throws IOException;
 }
diff --git a/briar-api/src/org/briarproject/api/sync/SubscriptionAck.java b/briar-api/src/org/briarproject/api/sync/SubscriptionAck.java
deleted file mode 100644
index b65ccf1c279116b8bc14aa7656265d8aa6436014..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/sync/SubscriptionAck.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.briarproject.api.sync;
-
-/** A packet acknowledging a {@link SubscriptionUpdate}. */
-public class SubscriptionAck {
-
-	private final long version;
-
-	public SubscriptionAck(long version) {
-		this.version = version;
-	}
-
-	/** Returns the version number of the acknowledged update. */
-	public long getVersion() {
-		return version;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/sync/SubscriptionUpdate.java b/briar-api/src/org/briarproject/api/sync/SubscriptionUpdate.java
deleted file mode 100644
index 0fabdf27836282b556aef156a22f3965f516f923..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/sync/SubscriptionUpdate.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.briarproject.api.sync;
-
-import java.util.Collection;
-
-/** A packet updating the recipient's view of the sender's subscriptions. */
-public class SubscriptionUpdate {
-
-	private final Collection<Group> groups;
-	private final long version;
-
-	public SubscriptionUpdate(Collection<Group> groups, long version) {
-		this.groups = groups;
-		this.version = version;
-	}
-
-	/**
-	 * Returns the groups to which the sender subscribes, and which the sender
-	 * has made visible to the recipient.
-	 */
-	public Collection<Group> getGroups() {
-		return groups;
-	}
-
-	/** Returns the update's version number. */
-	public long getVersion() {
-		return version;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/sync/SyncConstants.java b/briar-api/src/org/briarproject/api/sync/SyncConstants.java
index 3cb297a4bd30e6ba51521f4cc1006768e06d06d4..447a9aa9e5d530db29b7d7cc88b203b701314725 100644
--- a/briar-api/src/org/briarproject/api/sync/SyncConstants.java
+++ b/briar-api/src/org/briarproject/api/sync/SyncConstants.java
@@ -13,11 +13,8 @@ public interface SyncConstants {
 	/** The maximum length of the packet payload in bytes. */
 	int MAX_PACKET_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB
 
-	/** The maximum number of groups a user may subscribe to. */
-	int MAX_SUBSCRIPTIONS = 200;
-
 	/** The maximum length of a group descriptor in bytes. */
-	int MAX_GROUP_DESCRIPTOR_LENGTH = 100;
+	int MAX_GROUP_DESCRIPTOR_LENGTH = 100; // TODO: Remove
 
 	/** The maximum length of a message in bytes. */
 	int MAX_MESSAGE_LENGTH = MAX_PACKET_PAYLOAD_LENGTH - PACKET_HEADER_LENGTH;
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index 22cc9357f61a4065de80172b6cfad88449d67c8f..a5a22b135cd93aeae3bcd274ee09192af0220e7c 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -17,8 +17,6 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.transport.TransportKeys;
 
@@ -86,19 +84,11 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Adds a group to the given contact's subscriptions.
+	 * Stores a group.
 	 * <p>
 	 * Locking: write.
 	 */
-	void addContactGroup(T txn, ContactId c, Group g) throws DbException;
-
-	/**
-	 * Subscribes to a group, or returns false if the user already has the
-	 * maximum number of subscriptions.
-	 * <p>
-	 * Locking: write.
-	 */
-	boolean addGroup(T txn, Group g) throws DbException;
+	void addGroup(T txn, Group g) throws DbException;
 
 	/**
 	 * Stores a local pseudonym.
@@ -134,20 +124,20 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Stores a transport and returns true if the transport was not previously
-	 * in the database.
+	 * Stores a transport.
 	 * <p>
 	 * Locking: write.
 	 */
-	boolean addTransport(T txn, TransportId t, int maxLatency)
+	void addTransport(T txn, TransportId t, int maxLatency)
 			throws DbException;
 
 	/**
-	 * Stores the given transport keys for a newly added contact.
+	 * Stores transport keys for a newly added contact.
 	 * <p>
 	 * Locking: write.
 	 */
-	void addTransportKeys(T txn, ContactId c, TransportKeys k) throws DbException;
+	void addTransportKeys(T txn, ContactId c, TransportKeys k)
+			throws DbException;
 
 	/**
 	 * Makes a group visible to the given contact.
@@ -173,7 +163,7 @@ interface Database<T> {
 	boolean containsContact(T txn, ContactId c) throws DbException;
 
 	/**
-	 * Returns true if the user subscribes to the given group.
+	 * Returns true if the database contains the given group.
 	 * <p>
 	 * Locking: read.
 	 */
@@ -201,7 +191,7 @@ interface Database<T> {
 	boolean containsTransport(T txn, TransportId t) throws DbException;
 
 	/**
-	 * Returns true if the user subscribes to the given group and the group is
+	 * Returns true if the database contains the given group and the group is
 	 * visible to the given contact.
 	 * <p>
 	 * Locking: read.
@@ -225,14 +215,6 @@ interface Database<T> {
 	 */
 	int countOfferedMessages(T txn, ContactId c) throws DbException;
 
-	/**
-	 * Returns all groups belonging to the given client to which the user could
-	 * subscribe.
-	 * <p>
-	 * Locking: read.
-	 */
-	Collection<Group> getAvailableGroups(T txn, ClientId c) throws DbException;
-
 	/**
 	 * Returns the contact with the given ID.
 	 * <p>
@@ -276,7 +258,7 @@ interface Database<T> {
 	long getFreeSpace() throws DbException;
 
 	/**
-	 * Returns the group with the given ID, if the user subscribes to it.
+	 * Returns the group with the given ID.
 	 * <p>
 	 * Locking: read.
 	 */
@@ -290,8 +272,7 @@ interface Database<T> {
 	Metadata getGroupMetadata(T txn, GroupId g) throws DbException;
 
 	/**
-	 * Returns all groups belonging to the given client to which the user
-	 * subscribes.
+	 * Returns all groups belonging to the given client.
 	 * <p>
 	 * Locking: read.
 	 */
@@ -413,30 +394,6 @@ interface Database<T> {
 	 */
 	Settings getSettings(T txn, String namespace) throws DbException;
 
-	/**
-	 * Returns all contacts who subscribe to the given group.
-	 * <p>
-	 * Locking: read.
-	 */
-	Collection<Contact> getSubscribers(T txn, GroupId g) throws DbException;
-
-	/**
-	 * Returns a subscription ack for the given contact, or null if no ack is
-	 * due.
-	 * <p>
-	 * Locking: write.
-	 */
-	SubscriptionAck getSubscriptionAck(T txn, ContactId c) throws DbException;
-
-	/**
-	 * Returns a subscription update for the given contact and updates its
-	 * expiry time using the given latency, or returns null if no update is due.
-	 * <p>
-	 * Locking: write.
-	 */
-	SubscriptionUpdate getSubscriptionUpdate(T txn, ContactId c,
-			int maxLatency) throws DbException;
-
 	/**
 	 * Returns all transport keys for the given transport.
 	 * <p>
@@ -541,16 +498,14 @@ interface Database<T> {
 	void removeContact(T txn, ContactId c) throws DbException;
 
 	/**
-	 * Unsubscribes from a group. Any messages belonging to the group are
-	 * deleted from the database.
+	 * Removes a group (and all associated state) from the database.
 	 * <p>
 	 * Locking: write.
 	 */
 	void removeGroup(T txn, GroupId g) throws DbException;
 
 	/**
-	 * Removes a local pseudonym (and all associated contacts) from the
-	 * database.
+	 * Removes a local pseudonym (and all associated state) from the database.
 	 * <p>
 	 * Locking: write.
 	 */
@@ -643,32 +598,6 @@ interface Database<T> {
 	void setReorderingWindow(T txn, ContactId c, TransportId t,
 			long rotationPeriod, long base, byte[] bitmap) throws DbException;
 
-	/**
-	 * Updates the given contact's subscriptions and returns true, unless an
-	 * update with an equal or higher version number has already been received
-	 * from the contact.
-	 * <p>
-	 * Locking: write.
-	 */
-	boolean setGroups(T txn, ContactId c, Collection<Group> groups,
-			long version) throws DbException;
-
-	/**
-	 * Records a subscription ack from the given contact for the given version,
-	 * unless the contact has already acked an equal or higher version.
-	 * <p>
-	 * Locking: write.
-	 */
-	void setSubscriptionUpdateAcked(T txn, ContactId c, long version)
-			throws DbException;
-
-	/**
-	 * Makes a group visible or invisible to future contacts by default.
-	 * <p>
-	 * Locking: write.
-	 */
-	void setVisibleToAll(T txn, GroupId g, boolean all) throws DbException;
-
 	/**
 	 * Updates the transmission count and expiry time of the given message
 	 * with respect to the given contact, using the latency of the transport
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 505a288cd8988628bd8fb40a6473a06d2a960547..9e929d86c83865c158c4c58e3a4cab9f4b191109 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -7,17 +7,17 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.ContactExistsException;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.LocalAuthorExistsException;
-import org.briarproject.api.db.MessageExistsException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchContactException;
+import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.db.NoSuchLocalAuthorException;
 import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
 import org.briarproject.api.db.NoSuchTransportException;
 import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
@@ -26,10 +26,7 @@ import org.briarproject.api.event.MessageToRequestEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
-import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
-import org.briarproject.api.event.SubscriptionAddedEvent;
-import org.briarproject.api.event.SubscriptionRemovedEvent;
 import org.briarproject.api.event.TransportAddedEvent;
 import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.identity.Author;
@@ -46,8 +43,6 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.transport.TransportKeys;
 
@@ -164,30 +159,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void addContactGroup(ContactId c, Group g) throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				db.addContactGroup(txn, c, g);
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public boolean addGroup(Group g) throws DbException {
+	public void addGroup(Group g) throws DbException {
 		boolean added = false;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				if (!db.containsGroup(txn, g.getId()))
-					added = db.addGroup(txn, g);
+				if (!db.containsGroup(txn, g.getId())) {
+					db.addGroup(txn, g);
+					added = true;
+				}
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -196,8 +177,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		if (added) eventBus.broadcast(new SubscriptionAddedEvent(g));
-		return added;
+		if (added) eventBus.broadcast(new GroupAddedEvent(g));
 	}
 
 	public void addLocalAuthor(LocalAuthor a) throws DbException {
@@ -205,9 +185,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		try {
 			T txn = db.startTransaction();
 			try {
-				if (db.containsLocalAuthor(txn, a.getId()))
-					throw new LocalAuthorExistsException();
-				db.addLocalAuthor(txn, a);
+				if (!db.containsLocalAuthor(txn, a.getId())) {
+					db.addLocalAuthor(txn, a);
+				}
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -220,15 +200,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 
 	public void addLocalMessage(Message m, ClientId c, Metadata meta,
 			boolean shared) throws DbException {
+		boolean added = false;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				if (db.containsMessage(txn, m.getId()))
-					throw new MessageExistsException();
 				if (!db.containsGroup(txn, m.getGroupId()))
-					throw new NoSuchSubscriptionException();
-				addMessage(txn, m, VALID, shared, null);
+					throw new NoSuchGroupException();
+				if (!db.containsMessage(txn, m.getId())) {
+					addMessage(txn, m, VALID, shared, null);
+					added = true;
+				}
 				db.mergeMessageMetadata(txn, m.getId(), meta);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
@@ -238,8 +220,11 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		eventBus.broadcast(new MessageAddedEvent(m, null));
-		eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
+		if (added) {
+			eventBus.broadcast(new MessageAddedEvent(m, null));
+			eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
+			if (shared) eventBus.broadcast(new MessageSharedEvent(m));
+		}
 	}
 
 	/**
@@ -266,14 +251,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public boolean addTransport(TransportId t, int maxLatency)
-			throws DbException {
-		boolean added;
+	public void addTransport(TransportId t, int maxLatency) throws DbException {
+		boolean added = false;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				added = db.addTransport(txn, t, maxLatency);
+				if (!db.containsTransport(txn, t)) {
+					db.addTransport(txn, t, maxLatency);
+					added = true;
+				}
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -283,7 +270,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			lock.writeLock().unlock();
 		}
 		if (added) eventBus.broadcast(new TransportAddedEvent(t, maxLatency));
-		return added;
 	}
 
 	public void addTransportKeys(ContactId c, TransportKeys k)
@@ -434,64 +420,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return Collections.unmodifiableList(messages);
 	}
 
-	public SubscriptionAck generateSubscriptionAck(ContactId c)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsContact(txn, c))
-					throw new NoSuchContactException();
-				SubscriptionAck a = db.getSubscriptionAck(txn, c);
-				db.commitTransaction(txn);
-				return a;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public SubscriptionUpdate generateSubscriptionUpdate(ContactId c,
-			int maxLatency) throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsContact(txn, c))
-					throw new NoSuchContactException();
-				SubscriptionUpdate u =
-						db.getSubscriptionUpdate(txn, c, maxLatency);
-				db.commitTransaction(txn);
-				return u;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public Collection<Group> getAvailableGroups(ClientId c) throws DbException {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<Group> groups = db.getAvailableGroups(txn, c);
-				db.commitTransaction(txn);
-				return groups;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
-	}
-
 	public Contact getContact(ContactId c) throws DbException {
 		lock.readLock().lock();
 		try {
@@ -570,7 +498,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				Group group = db.getGroup(txn, g);
 				db.commitTransaction(txn);
 				return group;
@@ -589,7 +517,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				Metadata metadata = db.getGroupMetadata(txn, g);
 				db.commitTransaction(txn);
 				return metadata;
@@ -699,7 +627,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				Map<MessageId, Metadata> metadata =
 						db.getMessageMetadata(txn, g);
 				db.commitTransaction(txn);
@@ -741,7 +669,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				if (!db.containsContact(txn, c))
 					throw new NoSuchContactException();
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				Collection<MessageStatus> statuses =
 						db.getMessageStatus(txn, c, g);
 				db.commitTransaction(txn);
@@ -794,23 +722,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public Collection<Contact> getSubscribers(GroupId g) throws DbException {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<Contact> contacts = db.getSubscribers(txn, g);
-				db.commitTransaction(txn);
-				return contacts;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
-	}
-
 	public Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
 			throws DbException {
 		lock.readLock().lock();
@@ -857,7 +768,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				Collection<ContactId> visible = db.getVisibility(txn, g);
 				db.commitTransaction(txn);
 				return visible;
@@ -891,6 +802,28 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
+	public boolean isVisibleToContact(ContactId c, GroupId g)
+			throws DbException {
+		lock.readLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
+				if (!db.containsGroup(txn, g))
+					throw new NoSuchGroupException();
+				boolean visible = db.containsVisibleGroup(txn, c, g);
+				db.commitTransaction(txn);
+				return visible;
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
 	public void mergeGroupMetadata(GroupId g, Metadata meta)
 			throws DbException {
 		lock.writeLock().lock();
@@ -898,7 +831,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				db.mergeGroupMetadata(txn, g, meta);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
@@ -990,7 +923,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				duplicate = db.containsMessage(txn, m.getId());
 				visible = db.containsVisibleGroup(txn, c, m.getGroupId());
 				if (visible) {
-					if (!duplicate) addMessage(txn, m, UNKNOWN, true, c);
+					if (!duplicate) addMessage(txn, m, UNKNOWN, false, c);
 					db.raiseAckFlag(txn, c, m.getId());
 				}
 				db.commitTransaction(txn);
@@ -1002,8 +935,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			lock.writeLock().unlock();
 		}
 		if (visible) {
-			if (!duplicate)
-				eventBus.broadcast(new MessageAddedEvent(m, c));
+			if (!duplicate) eventBus.broadcast(new MessageAddedEvent(m, c));
 			eventBus.broadcast(new MessageToAckEvent(c));
 		}
 	}
@@ -1066,46 +998,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (requested) eventBus.broadcast(new MessageRequestedEvent(c));
 	}
 
-	public void receiveSubscriptionAck(ContactId c, SubscriptionAck a)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsContact(txn, c))
-					throw new NoSuchContactException();
-				db.setSubscriptionUpdateAcked(txn, c, a.getVersion());
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u)
-			throws DbException {
-		boolean updated;
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsContact(txn, c))
-					throw new NoSuchContactException();
-				updated = db.setGroups(txn, c, u.getGroups(), u.getVersion());
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (updated) eventBus.broadcast(new RemoteSubscriptionsUpdatedEvent(c));
-	}
-
 	public void removeContact(ContactId c) throws DbException {
 		lock.writeLock().lock();
 		try {
@@ -1132,7 +1024,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			try {
 				GroupId id = g.getId();
 				if (!db.containsGroup(txn, id))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				affected = db.getVisibility(txn, id);
 				db.removeGroup(txn, id);
 				db.commitTransaction(txn);
@@ -1143,8 +1035,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		eventBus.broadcast(new SubscriptionRemovedEvent(g));
-		eventBus.broadcast(new LocalSubscriptionsUpdatedEvent(affected));
+		eventBus.broadcast(new GroupRemovedEvent(g));
+		eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
 	}
 
 	public void removeLocalAuthor(AuthorId a) throws DbException {
@@ -1291,9 +1183,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
+					throw new NoSuchGroupException();
 				// Use HashSets for O(1) lookups, O(n) overall running time
-				HashSet<ContactId> now = new HashSet<ContactId>(visible);
+				Collection<ContactId> now = new HashSet<ContactId>(visible);
 				Collection<ContactId> before = db.getVisibility(txn, g);
 				before = new HashSet<ContactId>(before);
 				// Set the group's visibility for each current contact
@@ -1308,8 +1200,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 						affected.add(c);
 					}
 				}
-				// Make the group invisible to future contacts
-				db.setVisibleToAll(txn, g, false);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1319,30 +1209,23 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			lock.writeLock().unlock();
 		}
 		if (!affected.isEmpty())
-			eventBus.broadcast(new LocalSubscriptionsUpdatedEvent(affected));
+			eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
 	}
 
-	public void setVisibleToAll(GroupId g, boolean all) throws DbException {
-		Collection<ContactId> affected = new ArrayList<ContactId>();
+	public void setVisibleToContact(ContactId c, GroupId g, boolean visible)
+			throws DbException {
+		boolean wasVisible = false;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
 				if (!db.containsGroup(txn, g))
-					throw new NoSuchSubscriptionException();
-				// Make the group visible or invisible to future contacts
-				db.setVisibleToAll(txn, g, all);
-				if (all) {
-					// Make the group visible to all current contacts
-					Collection<ContactId> before = db.getVisibility(txn, g);
-					before = new HashSet<ContactId>(before);
-					for (ContactId c : db.getContactIds(txn)) {
-						if (!before.contains(c)) {
-							db.addVisibility(txn, c, g);
-							affected.add(c);
-						}
-					}
-				}
+					throw new NoSuchGroupException();
+				wasVisible = db.containsVisibleGroup(txn, c, g);
+				if (visible && !wasVisible) db.addVisibility(txn, c, g);
+				else if (!visible && wasVisible) db.removeVisibility(txn, c, g);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1351,8 +1234,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		if (!affected.isEmpty())
-			eventBus.broadcast(new LocalSubscriptionsUpdatedEvent(affected));
+		if (visible != wasVisible) {
+			eventBus.broadcast(new GroupVisibilityUpdatedEvent(
+					Collections.singletonList(c)));
+		}
 	}
 
 	public void updateTransportKeys(Map<ContactId, TransportKeys> keys)
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index 850ad42487f939bdcba15229a376647a07f5dee2..79c07da039a7ec53fe8a477c4d99be11dbfa76b9 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -20,8 +20,6 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.transport.IncomingKeys;
@@ -39,12 +37,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
@@ -54,7 +50,6 @@ import java.util.logging.Logger;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.db.Metadata.REMOVE;
 import static org.briarproject.api.db.StorageStatus.ADDING;
-import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS;
 import static org.briarproject.api.sync.ValidationManager.Validity.INVALID;
 import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
 import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
@@ -71,8 +66,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 19;
-	private static final int MIN_SCHEMA_VERSION = 19;
+	private static final int SCHEMA_VERSION = 20;
+	private static final int MIN_SCHEMA_VERSION = 20;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -109,7 +104,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " (groupId HASH NOT NULL,"
 					+ " clientId HASH NOT NULL,"
 					+ " descriptor BINARY NOT NULL,"
-					+ " visibleToAll BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (groupId))";
 
 	private static final String CREATE_GROUP_METADATA =
@@ -134,31 +128,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " REFERENCES groups (groupId)"
 					+ " ON DELETE CASCADE)";
 
-	private static final String CREATE_CONTACT_GROUPS =
-			"CREATE TABLE contactGroups"
-					+ " (contactId INT NOT NULL,"
-					+ " groupId HASH NOT NULL," // Not a foreign key
-					+ " clientId HASH NOT NULL,"
-					+ " descriptor BINARY NOT NULL,"
-					+ " PRIMARY KEY (contactId, groupId),"
-					+ " FOREIGN KEY (contactId)"
-					+ " REFERENCES contacts (contactId)"
-					+ " ON DELETE CASCADE)";
-
-	private static final String CREATE_GROUP_VERSIONS =
-			"CREATE TABLE groupVersions"
-					+ " (contactId INT NOT NULL,"
-					+ " localVersion BIGINT NOT NULL,"
-					+ " localAcked BIGINT NOT NULL,"
-					+ " remoteVersion BIGINT NOT NULL,"
-					+ " remoteAcked BOOLEAN NOT NULL,"
-					+ " expiry BIGINT NOT NULL,"
-					+ " txCount INT NOT NULL,"
-					+ " PRIMARY KEY (contactId),"
-					+ " FOREIGN KEY (contactid)"
-					+ " REFERENCES contacts (contactId)"
-					+ " ON DELETE CASCADE)";
-
 	private static final String CREATE_MESSAGES =
 			"CREATE TABLE messages"
 					+ " (messageId HASH NOT NULL,"
@@ -353,8 +322,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_GROUPS));
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_METADATA));
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES));
-			s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS));
-			s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS));
 			s.executeUpdate(insertTypeNames(CREATE_MESSAGES));
 			s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA));
 			s.executeUpdate(insertTypeNames(CREATE_OFFERS));
@@ -543,40 +510,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					if (rows != 1) throw new DbStateException();
 				ps.close();
 			}
-			// Make groups that are visible to everyone visible to this contact
-			sql = "SELECT groupId FROM groups WHERE visibleToAll = TRUE";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			ids = new ArrayList<byte[]>();
-			while (rs.next()) ids.add(rs.getBytes(1));
-			rs.close();
-			ps.close();
-			if (!ids.isEmpty()) {
-				sql = "INSERT INTO groupVisibilities (contactId, groupId)"
-						+ " VALUES (?, ?)";
-				ps = txn.prepareStatement(sql);
-				ps.setInt(1, c.getInt());
-				for (byte[] id : ids) {
-					ps.setBytes(2, id);
-					ps.addBatch();
-				}
-				int[] batchAffected = ps.executeBatch();
-				if (batchAffected.length != ids.size())
-					throw new DbStateException();
-				for (int rows : batchAffected)
-					if (rows != 1) throw new DbStateException();
-				ps.close();
-			}
-			// Create a group version row
-			sql = "INSERT INTO groupVersions (contactId, localVersion,"
-					+ " localAcked, remoteVersion, remoteAcked, expiry,"
-					+ " txCount)"
-					+ " VALUES (?, 1, 0, 0, TRUE, 0, 0)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
 			return c;
 		} catch (SQLException e) {
 			tryToClose(rs);
@@ -585,57 +518,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addContactGroup(Connection txn, ContactId c, Group g)
-			throws DbException {
+	public void addGroup(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT NULL FROM contactGroups"
-					+ " WHERE contactId = ? AND groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, g.getId().getBytes());
-			rs = ps.executeQuery();
-			boolean found = rs.next();
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if (found) return;
-			sql = "INSERT INTO contactGroups"
-					+ " (contactId, groupId, clientId, descriptor)"
-					+ " VALUES (?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, g.getId().getBytes());
-			ps.setBytes(3, g.getClientId().getBytes());
-			ps.setBytes(4, g.getDescriptor());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public boolean addGroup(Connection txn, Group g) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
-			String sql = "SELECT COUNT (groupId) FROM groups";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			if (!rs.next()) throw new DbStateException();
-			int count = rs.getInt(1);
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if (count > MAX_SUBSCRIPTIONS) throw new DbStateException();
-			if (count == MAX_SUBSCRIPTIONS) return false;
-			sql = "INSERT INTO groups"
-					+ " (groupId, clientId, descriptor, visibleToAll)"
-					+ " VALUES (?, ?, ?, FALSE)";
+			String sql = "INSERT INTO groups (groupId, clientId, descriptor)"
+					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getId().getBytes());
 			ps.setBytes(2, g.getClientId().getBytes());
@@ -643,9 +530,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			return true;
 		} catch (SQLException e) {
-			tryToClose(rs);
 			tryToClose(ps);
 			throw new DbException(e);
 		}
@@ -750,23 +635,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean addTransport(Connection txn, TransportId t, int maxLatency)
+	public void addTransport(Connection txn, TransportId t, int maxLatency)
 			throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
-			// Return false if the transport is already in the database
-			String sql = "SELECT NULL FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setString(1, t.getString());
-			rs = ps.executeQuery();
-			boolean found = rs.next();
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if (found) return false;
-			// Create a transport row
-			sql = "INSERT INTO transports (transportId, maxLatency)"
+			String sql = "INSERT INTO transports (transportId, maxLatency)"
 					+ " VALUES (?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setString(1, t.getString());
@@ -774,10 +647,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			return true;
 		} catch (SQLException e) {
 			tryToClose(ps);
-			tryToClose(rs);
 			throw new DbException(e);
 		}
 	}
@@ -855,16 +726,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			// Bump the subscription version
-			sql = "UPDATE groupVersions"
-					+ " SET localVersion = localVersion + 1,"
-					+ " expiry = 0, txCount = 0"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -1072,39 +933,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<Group> getAvailableGroups(Connection txn, ClientId c)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT DISTINCT cg.groupId, cg.descriptor"
-					+ " FROM contactGroups AS cg"
-					+ " LEFT OUTER JOIN groups AS g"
-					+ " ON cg.groupId = g.groupId"
-					+ " WHERE cg.clientId = ?"
-					+ " AND g.groupId IS NULL"
-					+ " GROUP BY cg.groupId";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, c.getBytes());
-			rs = ps.executeQuery();
-			List<Group> groups = new ArrayList<Group>();
-			Set<GroupId> ids = new HashSet<GroupId>();
-			while (rs.next()) {
-				GroupId id = new GroupId(rs.getBytes(1));
-				if (!ids.add(id)) throw new DbStateException();
-				byte[] descriptor = rs.getBytes(2);
-				groups.add(new Group(id, c, descriptor));
-			}
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(groups);
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Contact getContact(Connection txn, ContactId c) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -1474,15 +1302,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT m.messageId FROM messages AS m"
-					+ " JOIN contactGroups AS cg"
-					+ " ON m.groupId = cg.groupId"
 					+ " JOIN groupVisibilities AS gv"
 					+ " ON m.groupId = gv.groupId"
-					+ " AND cg.contactId = gv.contactId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " AND cg.contactId = s.contactId"
-					+ " WHERE cg.contactId = ?"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ?"
 					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE AND requested = FALSE"
 					+ " AND s.expiry < ?"
@@ -1536,15 +1361,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT length, m.messageId FROM messages AS m"
-					+ " JOIN contactGroups AS cg"
-					+ " ON m.groupId = cg.groupId"
 					+ " JOIN groupVisibilities AS gv"
 					+ " ON m.groupId = gv.groupId"
-					+ " AND cg.contactId = gv.contactId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " AND cg.contactId = s.contactId"
-					+ " WHERE cg.contactId = ?"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ?"
 					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE"
 					+ " AND s.expiry < ?"
@@ -1625,15 +1447,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT length, m.messageId FROM messages AS m"
-					+ " JOIN contactGroups AS cg"
-					+ " ON m.groupId = cg.groupId"
 					+ " JOIN groupVisibilities AS gv"
 					+ " ON m.groupId = gv.groupId"
-					+ " AND cg.contactId = gv.contactId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " AND cg.contactId = s.contactId"
-					+ " WHERE cg.contactId = ?"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ?"
 					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE AND requested = TRUE"
 					+ " AND s.expiry < ?"
@@ -1682,130 +1501,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<Contact> getSubscribers(Connection txn, GroupId g)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT c.contactId, authorId, c.name, publicKey,"
-					+ " localAuthorId, status"
-					+ " FROM contacts AS c"
-					+ " JOIN contactGroups AS cg"
-					+ " ON c.contactId = cg.contactId"
-					+ " WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			rs = ps.executeQuery();
-			List<Contact> contacts = new ArrayList<Contact>();
-			while (rs.next()) {
-				ContactId contactId = new ContactId(rs.getInt(1));
-				AuthorId authorId = new AuthorId(rs.getBytes(2));
-				String name = rs.getString(3);
-				byte[] publicKey = rs.getBytes(4);
-				Author author = new Author(authorId, name, publicKey);
-				AuthorId localAuthorId = new AuthorId(rs.getBytes(5));
-				StorageStatus status = StorageStatus.fromValue(rs.getInt(6));
-				contacts.add(new Contact(contactId, author, localAuthorId,
-						status));
-			}
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(contacts);
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public SubscriptionAck getSubscriptionAck(Connection txn, ContactId c)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT remoteVersion FROM groupVersions"
-					+ " WHERE contactId = ? AND remoteAcked = FALSE";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			rs = ps.executeQuery();
-			if (!rs.next()) {
-				rs.close();
-				ps.close();
-				return null;
-			}
-			long version = rs.getLong(1);
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			sql = "UPDATE groupVersions SET remoteAcked = TRUE"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-			return new SubscriptionAck(version);
-		} catch (SQLException e) {
-			tryToClose(ps);
-			tryToClose(rs);
-			throw new DbException(e);
-		}
-	}
-
-	public SubscriptionUpdate getSubscriptionUpdate(Connection txn, ContactId c,
-			int maxLatency) throws DbException {
-		long now = clock.currentTimeMillis();
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT g.groupId, clientId, descriptor,"
-					+ " localVersion, txCount"
-					+ " FROM groups AS g"
-					+ " JOIN groupVisibilities AS gvis"
-					+ " ON g.groupId = gvis.groupId"
-					+ " JOIN groupVersions AS gver"
-					+ " ON gvis.contactId = gver.contactId"
-					+ " WHERE gvis.contactId = ?"
-					+ " AND localVersion > localAcked"
-					+ " AND expiry < ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setLong(2, now);
-			rs = ps.executeQuery();
-			List<Group> groups = new ArrayList<Group>();
-			Set<GroupId> ids = new HashSet<GroupId>();
-			long version = 0;
-			int txCount = 0;
-			while (rs.next()) {
-				GroupId id = new GroupId(rs.getBytes(1));
-				if (!ids.add(id)) throw new DbStateException();
-				ClientId clientId = new ClientId(rs.getBytes(2));
-				byte[] descriptor = rs.getBytes(3);
-				groups.add(new Group(id, clientId, descriptor));
-				version = rs.getLong(4);
-				txCount = rs.getInt(5);
-			}
-			rs.close();
-			ps.close();
-			if (groups.isEmpty()) return null;
-			sql = "UPDATE groupVersions"
-					+ " SET expiry = ?, txCount = txCount + 1"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setLong(1, calculateExpiry(now, maxLatency, txCount));
-			ps.setInt(2, c.getInt());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-			groups = Collections.unmodifiableList(groups);
-			return new SubscriptionUpdate(groups, version);
-		} catch (SQLException e) {
-			tryToClose(ps);
-			tryToClose(rs);
-			throw new DbException(e);
-		}
-	}
-
 	public Map<ContactId, TransportKeys> getTransportKeys(Connection txn,
 			TransportId t) throws DbException {
 		PreparedStatement ps = null;
@@ -2187,44 +1882,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public void removeGroup(Connection txn, GroupId g) throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
-			// Find out which contacts are affected
-			String sql = "SELECT contactId FROM groupVisibilities"
-					+ " WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			rs = ps.executeQuery();
-			Collection<Integer> visible = new ArrayList<Integer>();
-			while (rs.next()) visible.add(rs.getInt(1));
-			rs.close();
-			ps.close();
-			// Delete the group
-			sql = "DELETE FROM groups WHERE groupId = ?";
+			String sql = "DELETE FROM groups WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			if (visible.isEmpty()) return;
-			// Bump the subscription versions for the affected contacts
-			sql = "UPDATE groupVersions"
-					+ " SET localVersion = localVersion + 1, expiry = 0"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			for (Integer c : visible) {
-				ps.setInt(1, c);
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if (batchAffected.length != visible.size())
-				throw new DbStateException();
-			for (int rows : batchAffected)
-				if (rows != 1) throw new DbStateException();
-			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
-			tryToClose(rs);
 			throw new DbException(e);
 		}
 	}
@@ -2331,15 +1997,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			// Bump the subscription version
-			sql = "UPDATE groupVersions"
-					+ " SET localVersion = localVersion + 1, expiry = 0"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -2454,130 +2111,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean setGroups(Connection txn, ContactId c,
-			Collection<Group> groups, long version) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			// Mark the update as needing to be acked
-			String sql = "UPDATE groupVersions"
-					+ " SET remoteVersion = ?, remoteAcked = FALSE"
-					+ " WHERE contactId = ? AND remoteVersion < ?";
-			ps = txn.prepareStatement(sql);
-			ps.setLong(1, version);
-			ps.setInt(2, c.getInt());
-			ps.setLong(3, version);
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-			// Return false if the update is obsolete
-			if (affected == 0) return false;
-			// Find any messages in groups that are being removed
-			Set<GroupId> newIds = new HashSet<GroupId>();
-			for (Group g : groups) newIds.add(g.getId());
-			sql = "SELECT messageId, m.groupId"
-					+ " FROM messages AS m"
-					+ " JOIN contactGroups AS cg"
-					+ " ON m.groupId = cg.groupId"
-					+ " WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			rs = ps.executeQuery();
-			List<MessageId> removed = new ArrayList<MessageId>();
-			while (rs.next()) {
-				if (!newIds.contains(new GroupId(rs.getBytes(2))))
-					removed.add(new MessageId(rs.getBytes(1)));
-			}
-			rs.close();
-			ps.close();
-			// Reset any statuses for messages in groups that are being removed
-			if (!removed.isEmpty()) {
-				sql = "UPDATE statuses SET ack = FALSE, seen = FALSE,"
-						+ " requested = FALSE, expiry = 0, txCount = 0"
-						+ " WHERE contactId = ? AND messageId = ?";
-				ps = txn.prepareStatement(sql);
-				ps.setInt(1, c.getInt());
-				for (MessageId m : removed) {
-					ps.setBytes(2, m.getBytes());
-					ps.addBatch();
-				}
-				int[] batchAffected = ps.executeBatch();
-				if (batchAffected.length != removed.size())
-					throw new DbStateException();
-				for (int rows : batchAffected)
-					if (rows < 0) throw new DbStateException();
-				ps.close();
-			}
-			// Delete the existing subscriptions, if any
-			sql = "DELETE FROM contactGroups WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.executeUpdate();
-			// Store the new subscriptions, if any
-			if (groups.isEmpty()) return true;
-			sql = "INSERT INTO contactGroups"
-					+ " (contactId, groupId, clientId, descriptor)"
-					+ " VALUES (?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			for (Group g : groups) {
-				ps.setBytes(2, g.getId().getBytes());
-				ps.setBytes(3, g.getClientId().getBytes());
-				ps.setBytes(4, g.getDescriptor());
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if (batchAffected.length != groups.size())
-				throw new DbStateException();
-			for (int rows : batchAffected)
-				if (rows != 1) throw new DbStateException();
-			ps.close();
-			return true;
-		} catch (SQLException e) {
-			tryToClose(ps);
-			tryToClose(rs);
-			throw new DbException(e);
-		}
-	}
-
-	public void setSubscriptionUpdateAcked(Connection txn, ContactId c,
-			long version) throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "UPDATE groupVersions SET localAcked = ?"
-					+ " WHERE contactId = ?"
-					+ " AND localAcked < ? AND localVersion >= ?";
-			ps = txn.prepareStatement(sql);
-			ps.setLong(1, version);
-			ps.setInt(2, c.getInt());
-			ps.setLong(3, version);
-			ps.setLong(4, version);
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public void setVisibleToAll(Connection txn, GroupId g, boolean all)
-			throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBoolean(1, all);
-			ps.setBytes(2, g.getBytes());
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
 			int maxLatency) throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..98d905c2989f081368cb4ae104460d17f73ebe6d
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/ForumListValidator.java
@@ -0,0 +1,69 @@
+package org.briarproject.forum;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfReader;
+import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageValidator;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
+import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+
+class ForumListValidator implements MessageValidator {
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumListValidator.class.getName());
+
+	private final BdfReaderFactory bdfReaderFactory;
+	private final MetadataEncoder metadataEncoder;
+
+	ForumListValidator(BdfReaderFactory bdfReaderFactory,
+			MetadataEncoder metadataEncoder) {
+		this.bdfReaderFactory = bdfReaderFactory;
+		this.metadataEncoder = metadataEncoder;
+	}
+
+	@Override
+	public Metadata validateMessage(Message m, Group g) {
+		try {
+			// Parse the message body
+			byte[] raw = m.getRaw();
+			ByteArrayInputStream in = new ByteArrayInputStream(raw,
+					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
+			BdfReader r = bdfReaderFactory.createReader(in);
+			r.readListStart();
+			long version = r.readInteger();
+			if (version < 0) throw new FormatException();
+			r.readListStart();
+			while (!r.hasListEnd()) {
+				r.readListStart();
+				String name = r.readString(MAX_FORUM_NAME_LENGTH);
+				if (name.length() == 0) throw new FormatException();
+				byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
+				if (salt.length != FORUM_SALT_LENGTH)
+					throw new FormatException();
+				r.readListEnd();
+			}
+			r.readListEnd();
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			// Return the metadata
+			BdfDictionary d = new BdfDictionary();
+			d.put("version", version);
+			d.put("local", false);
+			return metadataEncoder.encode(d);
+		} catch (IOException e) {
+			LOG.info("Invalid forum list");
+			return null;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 6b846d6714153d21a1b8ed36c01ab4ace2dfc633..0e2c581c84e293d97fbe61eb8f6ca947ad6cea18 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -4,13 +4,10 @@ import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
@@ -25,15 +22,12 @@ import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -42,6 +36,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
@@ -63,25 +58,23 @@ class ForumManagerImpl implements ForumManager {
 			Logger.getLogger(ForumManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final GroupFactory groupFactory;
+	private final ContactManager contactManager;
 	private final BdfReaderFactory bdfReaderFactory;
-	private final BdfWriterFactory bdfWriterFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
-	private final SecureRandom random;
+
+	/** Ensures isolation between database reads and writes. */
+	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 
 	@Inject
-	ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db,
-			GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
+	ForumManagerImpl(DatabaseComponent db, ContactManager contactManager,
+			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser) {
 		this.db = db;
-		this.groupFactory = groupFactory;
+		this.contactManager = contactManager;
 		this.bdfReaderFactory = bdfReaderFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
 		this.metadataEncoder = metadataEncoder;
 		this.metadataParser = metadataParser;
-		random = crypto.getSecureRandom();
 	}
 
 	@Override
@@ -89,221 +82,171 @@ class ForumManagerImpl implements ForumManager {
 		return CLIENT_ID;
 	}
 
-	@Override
-	public Forum createForum(String name) {
-		int length = StringUtils.toUtf8(name).length;
-		if (length == 0) throw new IllegalArgumentException();
-		if (length > MAX_FORUM_NAME_LENGTH)
-			throw new IllegalArgumentException();
-		byte[] salt = new byte[FORUM_SALT_LENGTH];
-		random.nextBytes(salt);
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		try {
-			w.writeListStart();
-			w.writeString(name);
-			w.writeRaw(salt);
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		Group g = groupFactory.createGroup(CLIENT_ID, out.toByteArray());
-		return new Forum(g, name);
-	}
-
-	@Override
-	public boolean addForum(Forum f) throws DbException {
-		return db.addGroup(f.getGroup());
-	}
-
 	@Override
 	public void addLocalPost(ForumPost p) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("timestamp", p.getMessage().getTimestamp());
-		if (p.getParent() != null) d.put("parent", p.getParent().getBytes());
-		if (p.getAuthor() != null) {
-			Author a = p.getAuthor();
-			BdfDictionary d1 = new BdfDictionary();
-			d1.put("id", a.getId().getBytes());
-			d1.put("name", a.getName());
-			d1.put("publicKey", a.getPublicKey());
-			d.put("author", d1);
-		}
-		d.put("contentType", p.getContentType());
-		d.put("local", true);
-		d.put("read", true);
+		lock.writeLock().lock();
 		try {
+			BdfDictionary d = new BdfDictionary();
+			d.put("timestamp", p.getMessage().getTimestamp());
+			if (p.getParent() != null)
+				d.put("parent", p.getParent().getBytes());
+			if (p.getAuthor() != null) {
+				Author a = p.getAuthor();
+				BdfDictionary d1 = new BdfDictionary();
+				d1.put("id", a.getId().getBytes());
+				d1.put("name", a.getName());
+				d1.put("publicKey", a.getPublicKey());
+				d.put("author", d1);
+			}
+			d.put("contentType", p.getContentType());
+			d.put("local", true);
+			d.put("read", true);
 			Metadata meta = metadataEncoder.encode(d);
 			db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	@Override
-	public Collection<Forum> getAvailableForums() throws DbException {
-		Collection<Group> groups = db.getAvailableGroups(CLIENT_ID);
-		List<Forum> forums = new ArrayList<Forum>(groups.size());
-		for (Group g : groups) {
-			try {
-				forums.add(parseForum(g));
-			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-		return Collections.unmodifiableList(forums);
-	}
-
-	private Forum parseForum(Group g) throws FormatException {
-		ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			String name = r.readString(MAX_FORUM_NAME_LENGTH);
-			if (name.length() == 0) throw new FormatException();
-			byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
-			if (salt.length != FORUM_SALT_LENGTH) throw new FormatException();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return new Forum(g, name);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
+		} finally {
+			lock.writeLock().unlock();
 		}
 	}
 
 	@Override
 	public Forum getForum(GroupId g) throws DbException {
-		Group group = db.getGroup(g);
-		if (!group.getClientId().equals(CLIENT_ID))
-			throw new IllegalArgumentException();
+		lock.readLock().lock();
 		try {
-			return parseForum(group);
+			return parseForum(db.getGroup(g));
 		} catch (FormatException e) {
-			throw new IllegalArgumentException();
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<Forum> getForums() throws DbException {
-		Collection<Group> groups = db.getGroups(CLIENT_ID);
-		List<Forum> forums = new ArrayList<Forum>(groups.size());
-		for (Group g : groups) {
-			try {
-				forums.add(parseForum(g));
-			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
+		lock.readLock().lock();
+		try {
+			List<Forum> forums = new ArrayList<Forum>();
+			for (Group g : db.getGroups(CLIENT_ID)) forums.add(parseForum(g));
+			return Collections.unmodifiableList(forums);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
-		return Collections.unmodifiableList(forums);
 	}
 
 	@Override
 	public byte[] getPostBody(MessageId m) throws DbException {
-		byte[] raw = db.getRawMessage(m);
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
+		lock.readLock().lock();
 		try {
-			// Extract the forum post body
+			byte[] raw = db.getRawMessage(m);
+			ByteArrayInputStream in = new ByteArrayInputStream(raw,
+					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
+			BdfReader r = bdfReaderFactory.createReader(in);
 			r.readListStart();
 			if (r.hasRaw()) r.skipRaw(); // Parent ID
 			else r.skipNull(); // No parent
 			if (r.hasList()) r.skipList(); // Author
 			else r.skipNull(); // No author
 			r.skipString(); // Content type
-			return r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
+			byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
+			if (r.hasRaw()) r.skipRaw(); // Signature
+			else r.skipNull();
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return postBody;
 		} catch (FormatException e) {
-			// Not a valid forum post
-			throw new IllegalArgumentException();
+			throw new DbException(e);
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<ForumPostHeader> getPostHeaders(GroupId g)
 			throws DbException {
-		// Load the IDs of the user's own identities and contacts' identities
-		Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
-		for (LocalAuthor a : db.getLocalAuthors())
-			localAuthorIds.add(a.getId());
-		Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
-		for (Contact c : db.getContacts())
-			contactAuthorIds.add(c.getAuthor().getId());
-		// Load and parse the metadata
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
-		Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>();
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			MessageId messageId = e.getKey();
-			Metadata meta = e.getValue();
-			try {
-				BdfDictionary d = metadataParser.parse(meta);
-				long timestamp = d.getInteger("timestamp");
-				Author author = null;
-				Author.Status authorStatus = ANONYMOUS;
-				BdfDictionary d1 = d.getDictionary("author", null);
-				if (d1 != null) {
-					AuthorId authorId = new AuthorId(d1.getRaw("id"));
-					String name = d1.getString("name");
-					byte[] publicKey = d1.getRaw("publicKey");
-					author = new Author(authorId, name, publicKey);
-					if (localAuthorIds.contains(authorId))
-						authorStatus = VERIFIED;
-					else if (contactAuthorIds.contains(authorId))
-						authorStatus = VERIFIED;
-					else authorStatus = UNKNOWN;
+		lock.readLock().lock();
+		try {
+			// Load the IDs of the user's identities
+			Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
+			for (LocalAuthor a : db.getLocalAuthors())
+				localAuthorIds.add(a.getId());
+			// Load the IDs of contacts' identities
+			Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
+			for (Contact c : contactManager.getContacts())
+				contactAuthorIds.add(c.getAuthor().getId());
+			// Load and parse the metadata
+			Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+			Collection<ForumPostHeader> headers =
+					new ArrayList<ForumPostHeader>();
+			for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+				MessageId messageId = e.getKey();
+				Metadata meta = e.getValue();
+				try {
+					BdfDictionary d = metadataParser.parse(meta);
+					long timestamp = d.getInteger("timestamp");
+					Author author = null;
+					Author.Status authorStatus = ANONYMOUS;
+					BdfDictionary d1 = d.getDictionary("author", null);
+					if (d1 != null) {
+						AuthorId authorId = new AuthorId(d1.getRaw("id"));
+						String name = d1.getString("name");
+						byte[] publicKey = d1.getRaw("publicKey");
+						author = new Author(authorId, name, publicKey);
+						if (localAuthorIds.contains(authorId))
+							authorStatus = VERIFIED;
+						else if (contactAuthorIds.contains(authorId))
+							authorStatus = VERIFIED;
+						else authorStatus = UNKNOWN;
+					}
+					String contentType = d.getString("contentType");
+					boolean read = d.getBoolean("read");
+					headers.add(new ForumPostHeader(messageId, timestamp,
+							author, authorStatus, contentType, read));
+				} catch (FormatException ex) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, ex.toString(), ex);
 				}
-				String contentType = d.getString("contentType");
-				boolean read = d.getBoolean("read");
-				headers.add(new ForumPostHeader(messageId, timestamp, author,
-						authorStatus, contentType, read));
-			} catch (FormatException ex) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, ex.toString(), ex);
 			}
+			return headers;
+		} finally {
+			lock.readLock().unlock();
 		}
-		return headers;
-	}
-
-	@Override
-	public Collection<Contact> getSubscribers(GroupId g) throws DbException {
-		return db.getSubscribers(g);
-	}
-
-	@Override
-	public Collection<ContactId> getVisibility(GroupId g) throws DbException {
-		return db.getVisibility(g);
-	}
-
-	@Override
-	public void removeForum(Forum f) throws DbException {
-		db.removeGroup(f.getGroup());
 	}
 
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("read", read);
+		lock.writeLock().lock();
 		try {
+			BdfDictionary d = new BdfDictionary();
+			d.put("read", read);
 			db.mergeMessageMetadata(m, metadataEncoder.encode(d));
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
+		} finally {
+			lock.writeLock().unlock();
 		}
 	}
 
-	@Override
-	public void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException {
-		db.setVisibility(g, visible);
-	}
-
-	@Override
-	public void setVisibleToAll(GroupId g, boolean all) throws DbException {
-		db.setVisibleToAll(g, all);
+	private Forum parseForum(Group g) throws FormatException {
+		ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
+		BdfReader r = bdfReaderFactory.createReader(in);
+		try {
+			r.readListStart();
+			String name = r.readString(MAX_FORUM_NAME_LENGTH);
+			byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return new Forum(g, name, salt);
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayInputStream
+			throw new RuntimeException(e);
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index 9dd05f22d69963fb7d209bfa106e63e490b40263..fc24c6c00cf1835bf0e23521b9084e007ed5c58f 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -3,39 +3,63 @@ package org.briarproject.forum;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.data.ObjectReader;
+import org.briarproject.api.event.EventBus;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostFactory;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.system.Clock;
 
 import javax.inject.Singleton;
 
-import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID;
-
 public class ForumModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
-		bind(ForumManager.class).to(ForumManagerImpl.class);
+		bind(ForumManager.class).to(ForumManagerImpl.class).in(Singleton.class);
 		bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class);
 	}
 
 	@Provides @Singleton
-	ForumPostValidator getValidator(ValidationManager validationManager,
-			CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
+	ForumPostValidator getForumPostValidator(
+			ValidationManager validationManager, CryptoComponent crypto,
+			BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory,
 			ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder,
 			Clock clock) {
 		ForumPostValidator validator = new ForumPostValidator(crypto,
 				bdfReaderFactory, bdfWriterFactory, authorReader,
 				metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(
+				ForumManagerImpl.CLIENT_ID, validator);
+		return validator;
+	}
+
+	@Provides @Singleton
+	ForumListValidator getForumListValidator(
+			ValidationManager validationManager,
+			BdfReaderFactory bdfReaderFactory,
+			MetadataEncoder metadataEncoder) {
+		ForumListValidator validator = new ForumListValidator(bdfReaderFactory,
+				metadataEncoder);
+		validationManager.registerMessageValidator(
+				ForumSharingManagerImpl.CLIENT_ID, validator);
 		return validator;
 	}
+
+	@Provides @Singleton
+	ForumSharingManager getForumSharingManager(ContactManager contactManager,
+			EventBus eventBus, ForumSharingManagerImpl forumSharingManager) {
+		contactManager.registerAddContactHook(forumSharingManager);
+		contactManager.registerRemoveContactHook(forumSharingManager);
+		eventBus.addListener(forumSharingManager);
+		return forumSharingManager;
+	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index 1ee770cf1875bc3294df0ee9151cd87891110b80..a21f546252e5c3151e6bada39fe02cf2506ccf9f 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -15,6 +15,7 @@ import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.data.ObjectReader;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageValidator;
@@ -26,8 +27,6 @@ import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
@@ -47,7 +46,6 @@ class ForumPostValidator implements MessageValidator {
 	private final Clock clock;
 	private final KeyParser keyParser;
 
-	@Inject
 	ForumPostValidator(CryptoComponent crypto,
 			BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory,
@@ -63,7 +61,7 @@ class ForumPostValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	public Metadata validateMessage(Message m, Group g) {
 		// Reject the message if it's too far in the future
 		long now = clock.currentTimeMillis();
 		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
@@ -78,8 +76,7 @@ class ForumPostValidator implements MessageValidator {
 			BdfReader r = bdfReaderFactory.createReader(in);
 			MessageId parent = null;
 			Author author = null;
-			String contentType;
-			byte[] postBody, sig = null;
+			byte[] sig = null;
 			r.readListStart();
 			// Read the parent ID, if any
 			if (r.hasRaw()) {
@@ -93,9 +90,9 @@ class ForumPostValidator implements MessageValidator {
 			if (r.hasList()) author = authorReader.readObject(r);
 			else r.readNull();
 			// Read the content type
-			contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
+			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
 			// Read the forum post body
-			postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
+			byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
 
 			// Read the signature, if any
 			if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH);
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f415051a1c7c5445f5174a2138bd62071000fcd1
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
@@ -0,0 +1,558 @@
+package org.briarproject.forum;
+
+import com.google.inject.Inject;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.contact.ContactManager.AddContactHook;
+import org.briarproject.api.contact.ContactManager.RemoveContactHook;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfReader;
+import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.BdfWriter;
+import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupFactory;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageFactory;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.sync.PrivateGroupFactory;
+import org.briarproject.api.system.Clock;
+import org.briarproject.util.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
+import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+
+class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
+		RemoveContactHook, EventListener {
+
+	static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
+			"cd11a5d04dccd9e2931d6fc3df456313"
+					+ "63bb3e9d9d0e9405fccdb051f41f5449"));
+
+	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumSharingManagerImpl.class.getName());
+
+	private final DatabaseComponent db;
+	private final Executor dbExecutor;
+	private final ContactManager contactManager;
+	private final ForumManager forumManager;
+	private final GroupFactory groupFactory;
+	private final PrivateGroupFactory privateGroupFactory;
+	private final MessageFactory messageFactory;
+	private final BdfReaderFactory bdfReaderFactory;
+	private final BdfWriterFactory bdfWriterFactory;
+	private final MetadataEncoder metadataEncoder;
+	private final MetadataParser metadataParser;
+	private final SecureRandom random;
+	private final Clock clock;
+	private final Group localGroup;
+
+	/** Ensures isolation between database reads and writes. */
+	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+	@Inject
+	ForumSharingManagerImpl(DatabaseComponent db,
+			@DatabaseExecutor Executor dbExecutor,
+			ContactManager contactManager, ForumManager forumManager,
+			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
+			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
+			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
+			MetadataParser metadataParser, SecureRandom random, Clock clock) {
+		this.db = db;
+		this.dbExecutor = dbExecutor;
+		this.contactManager = contactManager;
+		this.forumManager = forumManager;
+		this.groupFactory = groupFactory;
+		this.privateGroupFactory = privateGroupFactory;
+		this.messageFactory = messageFactory;
+		this.bdfReaderFactory = bdfReaderFactory;
+		this.bdfWriterFactory = bdfWriterFactory;
+		this.metadataEncoder = metadataEncoder;
+		this.metadataParser = metadataParser;
+		this.random = random;
+		this.clock = clock;
+		localGroup = groupFactory.createGroup(CLIENT_ID,
+				LOCAL_GROUP_DESCRIPTOR);
+	}
+
+	@Override
+	public void addingContact(ContactId c) {
+		lock.writeLock().lock();
+		try {
+			// Create a group to share with the contact
+			Group g = getContactGroup(db.getContact(c));
+			// Store the group and share it with the contact
+			db.addGroup(g);
+			db.setVisibility(g.getId(), Collections.singletonList(c));
+			// Attach the contact ID to the group
+			BdfDictionary d = new BdfDictionary();
+			d.put("contactId", c.getInt());
+			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
+			// Share any forums that are shared with all contacts
+			List<Forum> shared = getForumsSharedWithAllContacts();
+			storeMessage(g.getId(), shared, 0);
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void removingContact(ContactId c) {
+		lock.writeLock().lock();
+		try {
+			db.removeGroup(getContactGroup(db.getContact(c)));
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			ClientId c = m.getClientId();
+			if (m.isValid() && !m.isLocal() && c.equals(CLIENT_ID))
+				remoteForumsUpdated(m.getMessage().getGroupId());
+		}
+	}
+
+	@Override
+	public ClientId getClientId() {
+		return CLIENT_ID;
+	}
+
+	@Override
+	public Forum createForum(String name) {
+		int length = StringUtils.toUtf8(name).length;
+		if (length == 0) throw new IllegalArgumentException();
+		if (length > MAX_FORUM_NAME_LENGTH)
+			throw new IllegalArgumentException();
+		byte[] salt = new byte[FORUM_SALT_LENGTH];
+		random.nextBytes(salt);
+		return createForum(name, salt);
+	}
+
+	@Override
+	public void addForum(Forum f) throws DbException {
+		lock.writeLock().lock();
+		try {
+			db.addGroup(f.getGroup());
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void removeForum(Forum f) throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Update the list of forums shared with each contact
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				removeFromList(contactGroup.getId(), f);
+			}
+			db.removeGroup(f.getGroup());
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<Forum> getAvailableForums() throws DbException {
+		lock.readLock().lock();
+		try {
+			// Get any forums we subscribe to
+			Set<Group> subscribed = new HashSet<Group>(db.getGroups(
+					forumManager.getClientId()));
+			// Get all forums shared by contacts
+			Set<Forum> available = new HashSet<Forum>();
+			for (Contact c : contactManager.getContacts()) {
+				Group g = getContactGroup(c);
+				// Find the latest update version
+				LatestUpdate latest = findLatest(g.getId(), false);
+				if (latest != null) {
+					// Retrieve and parse the latest update
+					byte[] raw = db.getRawMessage(latest.messageId);
+					for (Forum f : parseForumList(raw)) {
+						if (!subscribed.contains(f.getGroup()))
+							available.add(f);
+					}
+				}
+			}
+			return Collections.unmodifiableSet(available);
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<Contact> getSharedBy(GroupId g) throws DbException {
+		lock.readLock().lock();
+		try {
+			List<Contact> subscribers = new ArrayList<Contact>();
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (listContains(contactGroup.getId(), g, false))
+					subscribers.add(c);
+			}
+			return Collections.unmodifiableList(subscribers);
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<ContactId> getSharedWith(GroupId g) throws DbException {
+		lock.readLock().lock();
+		try {
+			List<ContactId> shared = new ArrayList<ContactId>();
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (listContains(contactGroup.getId(), g, true))
+					shared.add(c.getId());
+			}
+			return Collections.unmodifiableList(shared);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public void setSharedWith(GroupId g, Collection<ContactId> shared)
+			throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Retrieve the forum
+			Forum f = parseForum(db.getGroup(g));
+			// Remove the forum from the list of forums shared with all contacts
+			removeFromList(localGroup.getId(), f);
+			// Update the list of forums shared with each contact
+			shared = new HashSet<ContactId>(shared);
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (shared.contains(c.getId())) {
+					if (addToList(contactGroup.getId(), f)) {
+						// If the contact is sharing the forum, make it visible
+						if (listContains(contactGroup.getId(), g, false))
+							db.setVisibleToContact(c.getId(), g, true);
+					}
+				} else {
+					removeFromList(contactGroup.getId(), f);
+					db.setVisibleToContact(c.getId(), g, false);
+				}
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void setSharedWithAll(GroupId g) throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Retrieve the forum
+			Forum f = parseForum(db.getGroup(g));
+			// Add the forum to the list of forums shared with all contacts
+			addToList(localGroup.getId(), f);
+			// Add the forum to the list of forums shared with each contact
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (addToList(contactGroup.getId(), f)) {
+					// If the contact is sharing the forum, make it visible
+					if (listContains(contactGroup.getId(), g, false))
+						db.setVisibleToContact(getContactId(g), g, true);
+				}
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	private Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	}
+
+	// Locking: lock.writeLock
+	private List<Forum> getForumsSharedWithAllContacts() throws DbException,
+			FormatException {
+		// Ensure the local group exists
+		db.addGroup(localGroup);
+		// Find the latest update in the local group
+		LatestUpdate latest = findLatest(localGroup.getId(), true);
+		if (latest == null) return Collections.emptyList();
+		// Retrieve and parse the latest update
+		return parseForumList(db.getRawMessage(latest.messageId));
+	}
+
+	// Locking: lock.readLock
+	private LatestUpdate findLatest(GroupId g, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = null;
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+			BdfDictionary d = metadataParser.parse(e.getValue());
+			if (d.getBoolean("local") != local) continue;
+			long version = d.getInteger("version");
+			if (latest == null || version > latest.version)
+				latest = new LatestUpdate(e.getKey(), version);
+		}
+		return latest;
+	}
+
+	private List<Forum> parseForumList(byte[] raw) throws FormatException {
+		List<Forum> forums = new ArrayList<Forum>();
+		ByteArrayInputStream in = new ByteArrayInputStream(raw,
+				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
+		BdfReader r = bdfReaderFactory.createReader(in);
+		try {
+			r.readListStart();
+			r.skipInteger(); // Version
+			r.readListStart();
+			while (!r.hasListEnd()) {
+				r.readListStart();
+				String name = r.readString(MAX_FORUM_NAME_LENGTH);
+				byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
+				r.readListEnd();
+				forums.add(createForum(name, salt));
+			}
+			r.readListEnd();
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return forums;
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayInputStream
+			throw new RuntimeException(e);
+		}
+	}
+
+	// Locking: lock.writeLock
+	private void storeMessage(GroupId g, List<Forum> forums, long version)
+			throws DbException, FormatException {
+		byte[] body = encodeForumList(forums, version);
+		long now = clock.currentTimeMillis();
+		Message m = messageFactory.createMessage(g, now, body);
+		BdfDictionary d = new BdfDictionary();
+		d.put("version", version);
+		d.put("local", true);
+		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), true);
+	}
+
+	private byte[] encodeForumList(List<Forum> forums, long version) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter w = bdfWriterFactory.createWriter(out);
+		try {
+			w.writeListStart();
+			w.writeInteger(version);
+			w.writeListStart();
+			for (Forum f : forums) {
+				w.writeListStart();
+				w.writeString(f.getName());
+				w.writeRaw(f.getSalt());
+				w.writeListEnd();
+			}
+			w.writeListEnd();
+			w.writeListEnd();
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayOutputStream
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+
+	private void remoteForumsUpdated(final GroupId g) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				lock.writeLock().lock();
+				try {
+					setForumVisibility(getContactId(g), getVisibleForums(g));
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} finally {
+					lock.writeLock().unlock();
+				}
+			}
+		});
+	}
+
+	// Locking: lock.readLock
+	private ContactId getContactId(GroupId contactGroupId) throws DbException,
+			FormatException {
+		Metadata meta = db.getGroupMetadata(contactGroupId);
+		BdfDictionary d = metadataParser.parse(meta);
+		int id = d.getInteger("contactId").intValue();
+		return new ContactId(id);
+	}
+
+	// Locking: lock.readLock
+	private Set<GroupId> getVisibleForums(GroupId contactGroupId)
+			throws DbException, FormatException {
+		// Get the latest local and remote updates
+		LatestUpdate local = findLatest(contactGroupId, true);
+		LatestUpdate remote = findLatest(contactGroupId, false);
+		// If there's no local and/or remote update, no forums are visible
+		if (local == null || remote == null) return Collections.emptySet();
+		// Intersect the sets of shared forums
+		byte[] localRaw = db.getRawMessage(local.messageId);
+		Set<Forum> shared = new HashSet<Forum>(parseForumList(localRaw));
+		byte[] remoteRaw = db.getRawMessage(remote.messageId);
+		shared.retainAll(parseForumList(remoteRaw));
+		// Forums in the intersection should be visible
+		Set<GroupId> visible = new HashSet<GroupId>(shared.size());
+		for (Forum f : shared) visible.add(f.getId());
+		return visible;
+	}
+
+	// Locking: lock.writeLock
+	private void setForumVisibility(ContactId c, Set<GroupId> visible)
+			throws DbException {
+		for (Group g : db.getGroups(forumManager.getClientId())) {
+			boolean isVisible = db.isVisibleToContact(c, g.getId());
+			boolean shouldBeVisible = visible.contains(g.getId());
+			if (isVisible && !shouldBeVisible)
+				db.setVisibleToContact(c, g.getId(), false);
+			else if (!isVisible && shouldBeVisible)
+				db.setVisibleToContact(c, g.getId(), true);
+		}
+	}
+
+	private Forum createForum(String name, byte[] salt) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter w = bdfWriterFactory.createWriter(out);
+		try {
+			w.writeListStart();
+			w.writeString(name);
+			w.writeRaw(salt);
+			w.writeListEnd();
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayOutputStream
+			throw new RuntimeException(e);
+		}
+		Group g = groupFactory.createGroup(forumManager.getClientId(),
+				out.toByteArray());
+		return new Forum(g, name, salt);
+	}
+
+	private Forum parseForum(Group g) throws FormatException {
+		ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
+		BdfReader r = bdfReaderFactory.createReader(in);
+		try {
+			r.readListStart();
+			String name = r.readString(MAX_FORUM_NAME_LENGTH);
+			byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return new Forum(g, name, salt);
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayInputStream
+			throw new RuntimeException(e);
+		}
+	}
+
+	// Locking: lock.readLock
+	private boolean listContains(GroupId g, GroupId forum, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = findLatest(g, local);
+		if (latest == null) return false;
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		for (Forum f : list) if (f.getId().equals(forum)) return true;
+		return false;
+	}
+
+	// Locking: lock.writeLock
+	private boolean addToList(GroupId g, Forum f) throws DbException,
+			FormatException {
+		LatestUpdate latest = findLatest(g, true);
+		if (latest == null) {
+			storeMessage(g, Collections.singletonList(f), 0);
+			return true;
+		}
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		if (list.contains(f)) return false;
+		list.add(f);
+		storeMessage(g, list, latest.version + 1);
+		return true;
+	}
+
+	// Locking: lock.writeLock
+	private void removeFromList(GroupId g, Forum f) throws DbException,
+			FormatException {
+		LatestUpdate latest = findLatest(g, true);
+		if (latest == null) return;
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		if (list.remove(f)) storeMessage(g, list, latest.version + 1);
+	}
+
+	private static class LatestUpdate {
+
+		private final MessageId messageId;
+		private final long version;
+
+		private LatestUpdate(MessageId messageId, long version) {
+			this.messageId = messageId;
+			this.version = version;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index ea1a852a90303d5f8072982f81e3fb9450fb8825..65f0432893f5d5d54c9806719b8d2be007f92cb6 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -5,6 +5,7 @@ import com.google.inject.Inject;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -50,18 +51,19 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			Logger.getLogger(MessagingManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
+	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final BdfReaderFactory bdfReaderFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
 
 	@Inject
-	MessagingManagerImpl(DatabaseComponent db,
+	MessagingManagerImpl(DatabaseComponent db, ContactManager contactManager,
 			PrivateGroupFactory privateGroupFactory,
-			BdfReaderFactory bdfReaderFactory,
-			MetadataEncoder metadataEncoder,
+			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser) {
 		this.db = db;
+		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
 		this.metadataEncoder = metadataEncoder;
@@ -71,11 +73,10 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public void addingContact(ContactId c) {
 		try {
-			// Create the conversation group
-			Group g = getConversationGroup(db.getContact(c));
-			// Subscribe to the group and share it with the contact
+			// Create a group to share with the contact
+			Group g = getContactGroup(db.getContact(c));
+			// Store the group and share it with the contact
 			db.addGroup(g);
-			db.addContactGroup(c, g);
 			db.setVisibility(g.getId(), Collections.singletonList(c));
 			// Attach the contact ID to the group
 			BdfDictionary d = new BdfDictionary();
@@ -88,14 +89,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 		}
 	}
 
-	private Group getConversationGroup(Contact c) {
+	private Group getContactGroup(Contact c) {
 		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
 	@Override
 	public void removingContact(ContactId c) {
 		try {
-			db.removeGroup(getConversationGroup(db.getContact(c)));
+			db.removeGroup(getContactGroup(db.getContact(c)));
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
@@ -136,7 +137,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public GroupId getConversationId(ContactId c) throws DbException {
-		return getConversationGroup(db.getContact(c)).getId();
+		return getContactGroup(contactManager.getContact(c)).getId();
 	}
 
 	@Override
@@ -173,15 +174,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 		BdfReader r = bdfReaderFactory.createReader(in);
 		try {
-			// Extract the private message body
 			r.readListStart();
 			if (r.hasRaw()) r.skipRaw(); // Parent ID
 			else r.skipNull(); // No parent
 			r.skipString(); // Content type
-			return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+			byte[] messageBody = r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return messageBody;
 		} catch (FormatException e) {
-			// Not a valid private message
-			throw new IllegalArgumentException();
+			throw new DbException(e);
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
index c0134934f1283334830f0910ca152eebb7917148..fa3e0ec5f3d9cd90c2d9cafa1b08089c7db290ca 100644
--- a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
+++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageValidator;
@@ -16,8 +17,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@@ -32,7 +31,6 @@ class PrivateMessageValidator implements MessageValidator {
 	private final MetadataEncoder metadataEncoder;
 	private final Clock clock;
 
-	@Inject
 	PrivateMessageValidator(BdfReaderFactory bdfReaderFactory,
 			MetadataEncoder metadataEncoder, Clock clock) {
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class PrivateMessageValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	public Metadata validateMessage(Message m, Group g) {
 		// Reject the message if it's too far in the future
 		long now = clock.currentTimeMillis();
 		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
@@ -55,7 +53,6 @@ class PrivateMessageValidator implements MessageValidator {
 					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 			BdfReader r = bdfReaderFactory.createReader(in);
 			MessageId parent = null;
-			String contentType;
 			r.readListStart();
 			// Read the parent ID, if any
 			if (r.hasRaw()) {
@@ -66,7 +63,7 @@ class PrivateMessageValidator implements MessageValidator {
 				r.readNull();
 			}
 			// Read the content type
-			contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
+			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
 			// Read the private message body
 			r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
 			r.readListEnd();
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 1df75db183f034f588f0184a951ecc8143226020..bbc6cd57d16f68433e7debf2e87977ced7094c3e 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -7,6 +7,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -19,7 +20,7 @@ import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
-import org.briarproject.api.db.NoSuchSubscriptionException;
+import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.api.properties.TransportPropertyManager;
 import org.briarproject.api.sync.ClientId;
@@ -60,6 +61,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			Logger.getLogger(TransportPropertyManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
+	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final MessageFactory messageFactory;
 	private final BdfReaderFactory bdfReaderFactory;
@@ -74,11 +76,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 
 	@Inject
 	TransportPropertyManagerImpl(DatabaseComponent db,
-			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
+			ContactManager contactManager, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory,
 			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser, Clock clock) {
 		this.db = db;
+		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.messageFactory = messageFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -96,9 +100,8 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		try {
 			// Create a group to share with the contact
 			Group g = getContactGroup(db.getContact(c));
-			// Subscribe to the group and share it with the contact
+			// Store the group and share it with the contact
 			db.addGroup(g);
-			db.addContactGroup(c, g);
 			db.setVisibility(g.getId(), Collections.singletonList(c));
 			// Copy the latest local properties into the group
 			DeviceId dev = db.getDeviceId();
@@ -110,49 +113,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} catch (FormatException e) {
-			throw new RuntimeException(e);
-		} catch (IOException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} finally {
 			lock.writeLock().unlock();
 		}
 	}
 
-	private Group getContactGroup(Contact c) {
-		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
-	}
-
-	private void storeMessage(GroupId g, DeviceId dev, TransportId t,
-			TransportProperties p, long version, boolean local, boolean shared)
-			throws DbException, IOException {
-		byte[] body = encodeProperties(dev, t, p, version);
-		long now = clock.currentTimeMillis();
-		Message m = messageFactory.createMessage(g, now, body);
-		BdfDictionary d = new BdfDictionary();
-		d.put("transportId", t.getString());
-		d.put("version", version);
-		d.put("local", local);
-		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared);
-	}
-
-	private byte[] encodeProperties(DeviceId dev, TransportId t,
-			TransportProperties p, long version) {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		try {
-			w.writeListStart();
-			w.writeRaw(dev.getBytes());
-			w.writeString(t.getString());
-			w.writeInteger(version);
-			w.writeDictionary(p);
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		return out.toByteArray();
-	}
-
 	@Override
 	public void removingContact(ContactId c) {
 		lock.writeLock().lock();
@@ -175,7 +141,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 				storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false,
 						false);
 			}
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
 		} finally {
 			lock.writeLock().unlock();
@@ -187,18 +153,18 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			throws DbException {
 		lock.readLock().lock();
 		try {
-			// Find the latest local version for each transport
+			// Find the latest local update for each transport
 			Map<TransportId, LatestUpdate> latest =
 					findLatest(localGroup.getId(), true);
-			// Retrieve and decode the latest local properties
+			// Retrieve and parse the latest local properties
 			Map<TransportId, TransportProperties> local =
 					new HashMap<TransportId, TransportProperties>();
 			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
 				byte[] raw = db.getRawMessage(e.getValue().messageId);
-				local.put(e.getKey(), decodeProperties(raw));
+				local.put(e.getKey(), parseProperties(raw));
 			}
 			return Collections.unmodifiableMap(local);
-		} catch (NoSuchSubscriptionException e) {
+		} catch (NoSuchGroupException e) {
 			// Local group doesn't exist - there are no local properties
 			return Collections.emptyMap();
 		} catch (IOException e) {
@@ -208,54 +174,17 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		}
 	}
 
-	private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
-			throws DbException, FormatException {
-		// TODO: Use metadata queries
-		Map<TransportId, LatestUpdate> latestUpdates =
-				new HashMap<TransportId, LatestUpdate>();
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			BdfDictionary d = metadataParser.parse(e.getValue());
-			if (d.getBoolean("local") != local) continue;
-			TransportId t = new TransportId(d.getString("transportId"));
-			long version = d.getInteger("version");
-			LatestUpdate latest = latestUpdates.get(t);
-			if (latest == null || version > latest.version)
-				latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
-		}
-		return latestUpdates;
-	}
-
-	private TransportProperties decodeProperties(byte[] raw)
-			throws IOException {
-		TransportProperties p = new TransportProperties();
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
-		r.readListStart();
-		r.skipRaw(); // Device ID
-		r.skipString(); // Transport ID
-		r.skipInteger(); // Version
-		r.readDictionaryStart();
-		while (!r.hasDictionaryEnd()) {
-			String key = r.readString(MAX_PROPERTY_LENGTH);
-			String value = r.readString(MAX_PROPERTY_LENGTH);
-			p.put(key, value);
-		}
-		return p;
-	}
-
 	@Override
 	public TransportProperties getLocalProperties(TransportId t)
 			throws DbException {
 		lock.readLock().lock();
 		try {
-			// Find the latest local version
-			LatestUpdate latest = findLatest(localGroup.getId(), true).get(t);
+			// Find the latest local update
+			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
 			if (latest == null) return null;
-			// Retrieve and decode the latest local properties
-			return decodeProperties(db.getRawMessage(latest.messageId));
-		} catch (NoSuchSubscriptionException e) {
+			// Retrieve and parse the latest local properties
+			return parseProperties(db.getRawMessage(latest.messageId));
+		} catch (NoSuchGroupException e) {
 			// Local group doesn't exist - there are no local properties
 			return null;
 		} catch (IOException e) {
@@ -272,14 +201,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		try {
 			Map<ContactId, TransportProperties> remote =
 					new HashMap<ContactId, TransportProperties>();
-			for (Contact c : db.getContacts())  {
+			for (Contact c : contactManager.getContacts())  {
 				Group g = getContactGroup(c);
-				// Find the latest remote version
-				LatestUpdate latest = findLatest(g.getId(), false).get(t);
+				// Find the latest remote update
+				LatestUpdate latest = findLatest(g.getId(), t, false);
 				if (latest != null) {
-					// Retrieve and decode the latest remote properties
+					// Retrieve and parse the latest remote properties
 					byte[] raw = db.getRawMessage(latest.messageId);
-					remote.put(c.getId(), decodeProperties(raw));
+					remote.put(c.getId(), parseProperties(raw));
 				}
 			}
 			return Collections.unmodifiableMap(remote);
@@ -299,12 +228,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			db.addGroup(localGroup);
 			// Merge the new properties with any existing properties
 			TransportProperties merged;
-			LatestUpdate latest = findLatest(localGroup.getId(), true).get(t);
+			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
 			if (latest == null) {
 				merged = p;
 			} else {
 				byte[] raw = db.getRawMessage(latest.messageId);
-				TransportProperties old = decodeProperties(raw);
+				TransportProperties old = parseProperties(raw);
 				merged = new TransportProperties(old);
 				merged.putAll(p);
 				if (merged.equals(old)) return; // Unchanged
@@ -315,9 +244,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			storeMessage(localGroup.getId(), dev, t, merged, version, true,
 					false);
 			// Store the merged properties in each contact's group
-			for (Contact c : db.getContacts()) {
+			for (Contact c : contactManager.getContacts()) {
 				Group g = getContactGroup(c);
-				latest = findLatest(g.getId(), true).get(t);
+				latest = findLatest(g.getId(), t, true);
 				version = latest == null ? 1 : latest.version + 1;
 				storeMessage(g.getId(), dev, t, merged, version, true, true);
 			}
@@ -328,6 +257,100 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		}
 	}
 
+	private Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	}
+
+	// Locking: lock.writeLock
+	private void storeMessage(GroupId g, DeviceId dev, TransportId t,
+			TransportProperties p, long version, boolean local, boolean shared)
+			throws DbException, FormatException {
+		byte[] body = encodeProperties(dev, t, p, version);
+		long now = clock.currentTimeMillis();
+		Message m = messageFactory.createMessage(g, now, body);
+		BdfDictionary d = new BdfDictionary();
+		d.put("transportId", t.getString());
+		d.put("version", version);
+		d.put("local", local);
+		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared);
+	}
+
+	private byte[] encodeProperties(DeviceId dev, TransportId t,
+			TransportProperties p, long version) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter w = bdfWriterFactory.createWriter(out);
+		try {
+			w.writeListStart();
+			w.writeRaw(dev.getBytes());
+			w.writeString(t.getString());
+			w.writeInteger(version);
+			w.writeDictionary(p);
+			w.writeListEnd();
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayOutputStream
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+
+	// Locking: lock.readLock
+	private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
+			throws DbException, FormatException {
+		Map<TransportId, LatestUpdate> latestUpdates =
+				new HashMap<TransportId, LatestUpdate>();
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+			BdfDictionary d = metadataParser.parse(e.getValue());
+			if (d.getBoolean("local") == local) {
+				TransportId t = new TransportId(d.getString("transportId"));
+				long version = d.getInteger("version");
+				LatestUpdate latest = latestUpdates.get(t);
+				if (latest == null || version > latest.version)
+					latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
+			}
+		}
+		return latestUpdates;
+	}
+
+	// Locking: lock.readLock
+	private LatestUpdate findLatest(GroupId g, TransportId t, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = null;
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+			BdfDictionary d = metadataParser.parse(e.getValue());
+			if (d.getString("transportId").equals(t.getString())
+					&& d.getBoolean("local") == local) {
+				long version = d.getInteger("version");
+				if (latest == null || version > latest.version)
+					latest = new LatestUpdate(e.getKey(), version);
+			}
+		}
+		return latest;
+	}
+
+	private TransportProperties parseProperties(byte[] raw)
+			throws IOException {
+		TransportProperties p = new TransportProperties();
+		ByteArrayInputStream in = new ByteArrayInputStream(raw,
+				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
+		BdfReader r = bdfReaderFactory.createReader(in);
+		r.readListStart();
+		r.skipRaw(); // Device ID
+		r.skipString(); // Transport ID
+		r.skipInteger(); // Version
+		r.readDictionaryStart();
+		while (!r.hasDictionaryEnd()) {
+			String key = r.readString(MAX_PROPERTY_LENGTH);
+			String value = r.readString(MAX_PROPERTY_LENGTH);
+			p.put(key, value);
+		}
+		r.readDictionaryEnd();
+		r.readListEnd();
+		if (!r.eof()) throw new FormatException();
+		return p;
+	}
+
 	private static class LatestUpdate {
 
 		private final MessageId messageId;
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
index eeee9f333447c2489c62e27d42da582eca6bde9c..a3b7c17832ac5997d3ece03b33418e5ebee9d3c7 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageValidator;
 import org.briarproject.api.system.Clock;
@@ -15,8 +16,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static org.briarproject.api.TransportId.MAX_TRANSPORT_ID_LENGTH;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
@@ -32,7 +31,6 @@ class TransportPropertyValidator implements MessageValidator {
 	private final MetadataEncoder metadataEncoder;
 	private final Clock clock;
 
-	@Inject
 	TransportPropertyValidator(BdfReaderFactory bdfReaderFactory,
 			MetadataEncoder metadataEncoder, Clock clock) {
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class TransportPropertyValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	public Metadata validateMessage(Message m, Group g) {
 		// Reject the message if it's too far in the future
 		long now = clock.currentTimeMillis();
 		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
diff --git a/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java b/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java
index da217728a3221daacd937b8d617f14a6e275b8fa..bfb768802179b112b345d8d675b9d8acc211c79d 100644
--- a/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java
+++ b/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java
@@ -16,6 +16,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.api.db.StorageStatus.ADDING;
 
+// TODO: Move this class to the identity package
 class AuthorFactoryImpl implements AuthorFactory {
 
 	private final CryptoComponent crypto;
diff --git a/briar-core/src/org/briarproject/sync/AuthorReader.java b/briar-core/src/org/briarproject/sync/AuthorReader.java
index 8a4b85300200ef9993d3a8b04aa6cb2f0213f497..cb697681eebfdf0d318679625fdb6a98d87a6c7b 100644
--- a/briar-core/src/org/briarproject/sync/AuthorReader.java
+++ b/briar-core/src/org/briarproject/sync/AuthorReader.java
@@ -11,6 +11,7 @@ import java.io.IOException;
 import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 
+// TODO: Move this class to the identity package
 class AuthorReader implements ObjectReader<Author> {
 
 	private final AuthorFactory authorFactory;
diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
index 29725fe81741c293093578e302692231cee403e3..f6ec4779598c335b2493ef1c070fba258978ee5b 100644
--- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
@@ -8,21 +8,17 @@ import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
+import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
-import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
 import org.briarproject.api.event.ShutdownEvent;
 import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.PacketWriter;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.SyncSession;
 import org.briarproject.api.system.Clock;
 
@@ -87,9 +83,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 	public void run() throws IOException {
 		eventBus.addListener(this);
 		try {
-			// Start a query for each type of packet, in order of urgency
-			dbExecutor.execute(new GenerateSubscriptionAck());
-			dbExecutor.execute(new GenerateSubscriptionUpdate());
+			// Start a query for each type of packet
 			dbExecutor.execute(new GenerateAck());
 			dbExecutor.execute(new GenerateBatch());
 			dbExecutor.execute(new GenerateOffer());
@@ -118,7 +112,6 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 						now = clock.currentTimeMillis();
 						if (now >= nextRetxQuery) {
 							// Check for retransmittable packets
-							dbExecutor.execute(new GenerateSubscriptionUpdate());
 							dbExecutor.execute(new GenerateBatch());
 							dbExecutor.execute(new GenerateOffer());
 							nextRetxQuery = now + RETX_QUERY_INTERVAL;
@@ -157,16 +150,10 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 			if (c.getContactId().equals(contactId)) interrupt();
 		} else if (e instanceof MessageSharedEvent) {
 			dbExecutor.execute(new GenerateOffer());
-		} else if (e instanceof MessageValidatedEvent) {
-			if (((MessageValidatedEvent) e).isValid())
+		} else if (e instanceof GroupVisibilityUpdatedEvent) {
+			GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
+			if (g.getAffectedContacts().contains(contactId))
 				dbExecutor.execute(new GenerateOffer());
-		} else if (e instanceof LocalSubscriptionsUpdatedEvent) {
-			LocalSubscriptionsUpdatedEvent l =
-					(LocalSubscriptionsUpdatedEvent) e;
-			if (l.getAffectedContacts().contains(contactId)) {
-				dbExecutor.execute(new GenerateSubscriptionUpdate());
-				dbExecutor.execute(new GenerateOffer());
-			}
 		} else if (e instanceof MessageRequestedEvent) {
 			if (((MessageRequestedEvent) e).getContactId().equals(contactId))
 				dbExecutor.execute(new GenerateBatch());
@@ -176,13 +163,6 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		} else if (e instanceof MessageToRequestEvent) {
 			if (((MessageToRequestEvent) e).getContactId().equals(contactId))
 				dbExecutor.execute(new GenerateRequest());
-		} else if (e instanceof RemoteSubscriptionsUpdatedEvent) {
-			RemoteSubscriptionsUpdatedEvent r =
-					(RemoteSubscriptionsUpdatedEvent) e;
-			if (r.getContactId().equals(contactId)) {
-				dbExecutor.execute(new GenerateSubscriptionAck());
-				dbExecutor.execute(new GenerateOffer());
-			}
 		} else if (e instanceof ShutdownEvent) {
 			interrupt();
 		} else if (e instanceof TransportRemovedEvent) {
@@ -332,75 +312,4 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 			dbExecutor.execute(new GenerateRequest());
 		}
 	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionAck implements Runnable {
-
-		public void run() {
-			if (interrupted) return;
-			try {
-				SubscriptionAck a = db.generateSubscriptionAck(contactId);
-				if (LOG.isLoggable(INFO))
-					LOG.info("Generated subscription ack: " + (a != null));
-				if (a != null) writerTasks.add(new WriteSubscriptionAck(a));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
-
-	// This tasks runs on the writer thread
-	private class WriteSubscriptionAck
-	implements ThrowingRunnable<IOException> {
-
-		private final SubscriptionAck ack;
-
-		private WriteSubscriptionAck(SubscriptionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() throws IOException {
-			if (interrupted) return;
-			packetWriter.writeSubscriptionAck(ack);
-			LOG.info("Sent subscription ack");
-			dbExecutor.execute(new GenerateSubscriptionAck());
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionUpdate implements Runnable {
-
-		public void run() {
-			if (interrupted) return;
-			try {
-				SubscriptionUpdate u =
-						db.generateSubscriptionUpdate(contactId, maxLatency);
-				if (LOG.isLoggable(INFO))
-					LOG.info("Generated subscription update: " + (u != null));
-				if (u != null) writerTasks.add(new WriteSubscriptionUpdate(u));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteSubscriptionUpdate
-	implements ThrowingRunnable<IOException> {
-
-		private final SubscriptionUpdate update;
-
-		private WriteSubscriptionUpdate(SubscriptionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() throws IOException {
-			if (interrupted) return;
-			packetWriter.writeSubscriptionUpdate(update);
-			LOG.info("Sent subscription update");
-			dbExecutor.execute(new GenerateSubscriptionUpdate());
-		}
-	}
 }
diff --git a/briar-core/src/org/briarproject/sync/GroupReader.java b/briar-core/src/org/briarproject/sync/GroupReader.java
deleted file mode 100644
index 2500c8b1c968ed69d628fcdc0d4f6575fe90b4ec..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/sync/GroupReader.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.briarproject.sync;
-
-import org.briarproject.api.FormatException;
-import org.briarproject.api.UniqueId;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.ObjectReader;
-import org.briarproject.api.sync.ClientId;
-import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupFactory;
-
-import java.io.IOException;
-
-import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
-
-class GroupReader implements ObjectReader<Group> {
-
-	private final GroupFactory groupFactory;
-
-	GroupReader(GroupFactory groupFactory) {
-		this.groupFactory = groupFactory;
-	}
-
-	public Group readObject(BdfReader r) throws IOException {
-		r.readListStart();
-		byte[] id = r.readRaw(UniqueId.LENGTH);
-		if (id.length != UniqueId.LENGTH) throw new FormatException();
-		byte[] descriptor = r.readRaw(MAX_GROUP_DESCRIPTOR_LENGTH);
-		r.readListEnd();
-		return groupFactory.createGroup(new ClientId(id), descriptor);
-	}
-}
diff --git a/briar-core/src/org/briarproject/sync/IncomingSession.java b/briar-core/src/org/briarproject/sync/IncomingSession.java
index 7c7444664ca84f99c426ab93dca6e8e333711dc0..6fbfdd2426ce1938f665f83d7dad59124b0dd22d 100644
--- a/briar-core/src/org/briarproject/sync/IncomingSession.java
+++ b/briar-core/src/org/briarproject/sync/IncomingSession.java
@@ -16,8 +16,6 @@ import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.PacketReader;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.SyncSession;
 
 import java.io.IOException;
@@ -69,12 +67,6 @@ class IncomingSession implements SyncSession, EventListener {
 				} else if (packetReader.hasRequest()) {
 					Request r = packetReader.readRequest();
 					dbExecutor.execute(new ReceiveRequest(r));
-				} else if (packetReader.hasSubscriptionAck()) {
-					SubscriptionAck a = packetReader.readSubscriptionAck();
-					dbExecutor.execute(new ReceiveSubscriptionAck(a));
-				} else if (packetReader.hasSubscriptionUpdate()) {
-					SubscriptionUpdate u = packetReader.readSubscriptionUpdate();
-					dbExecutor.execute(new ReceiveSubscriptionUpdate(u));
 				} else {
 					throw new FormatException();
 				}
@@ -172,40 +164,4 @@ class IncomingSession implements SyncSession, EventListener {
 			}
 		}
 	}
-
-	private class ReceiveSubscriptionAck implements Runnable {
-
-		private final SubscriptionAck ack;
-
-		private ReceiveSubscriptionAck(SubscriptionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			try {
-				db.receiveSubscriptionAck(contactId, ack);
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
-
-	private class ReceiveSubscriptionUpdate implements Runnable {
-
-		private final SubscriptionUpdate update;
-
-		private ReceiveSubscriptionUpdate(SubscriptionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			try {
-				db.receiveSubscriptionUpdate(contactId, update);
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
 }
diff --git a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
index e32ec110f31602461b2dc1de352b8660139e18c5..1015b7daf77a885a930ea0a3344c8c49d71eeeb4 100644
--- a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
+++ b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
@@ -10,8 +10,6 @@ import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.ByteUtils;
 
-import java.io.IOException;
-
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
@@ -25,8 +23,7 @@ class MessageFactoryImpl implements MessageFactory {
 	}
 
 	@Override
-	public Message createMessage(GroupId groupId, long timestamp, byte[] body)
-			throws IOException {
+	public Message createMessage(GroupId groupId, long timestamp, byte[] body) {
 		if (body.length > MAX_MESSAGE_BODY_LENGTH)
 			throw new IllegalArgumentException();
 		byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length];
diff --git a/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java b/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java
index eac4b931f9906d6e3a34cc105ea16713ac1a1148..aad376cc406eda6817cac9c35854d8b2a9574687 100644
--- a/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java
+++ b/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java
@@ -1,11 +1,8 @@
 package org.briarproject.sync;
 
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.ObjectReader;
 import org.briarproject.api.sync.PacketReader;
 import org.briarproject.api.sync.PacketReaderFactory;
-import org.briarproject.api.sync.SubscriptionUpdate;
 
 import java.io.InputStream;
 
@@ -14,20 +11,13 @@ import javax.inject.Inject;
 class PacketReaderFactoryImpl implements PacketReaderFactory {
 
 	private final CryptoComponent crypto;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader;
 
 	@Inject
-	PacketReaderFactoryImpl(CryptoComponent crypto,
-			BdfReaderFactory bdfReaderFactory,
-			ObjectReader<SubscriptionUpdate> subscriptionUpdateReader) {
+	PacketReaderFactoryImpl(CryptoComponent crypto) {
 		this.crypto = crypto;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.subscriptionUpdateReader = subscriptionUpdateReader;
 	}
 
 	public PacketReader createPacketReader(InputStream in) {
-		return new PacketReaderImpl(crypto, bdfReaderFactory,
-				subscriptionUpdateReader, in);
+		return new PacketReaderImpl(crypto, in);
 	}
 }
diff --git a/briar-core/src/org/briarproject/sync/PacketReaderImpl.java b/briar-core/src/org/briarproject/sync/PacketReaderImpl.java
index 8c9a10ada622c317903e837b5cdbda11397e2b71..4eea8831e8bce33edff90f80fefd2afbc2c45505 100644
--- a/briar-core/src/org/briarproject/sync/PacketReaderImpl.java
+++ b/briar-core/src/org/briarproject/sync/PacketReaderImpl.java
@@ -3,9 +3,6 @@ package org.briarproject.sync;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.ObjectReader;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
@@ -13,11 +10,8 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.PacketReader;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.util.ByteUtils;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -28,8 +22,6 @@ import static org.briarproject.api.sync.PacketTypes.ACK;
 import static org.briarproject.api.sync.PacketTypes.MESSAGE;
 import static org.briarproject.api.sync.PacketTypes.OFFER;
 import static org.briarproject.api.sync.PacketTypes.REQUEST;
-import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_ACK;
-import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_UPDATE;
 import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.PACKET_HEADER_LENGTH;
@@ -41,20 +33,14 @@ class PacketReaderImpl implements PacketReader {
 	private enum State { BUFFER_EMPTY, BUFFER_FULL, EOF }
 
 	private final CryptoComponent crypto;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader;
 	private final InputStream in;
 	private final byte[] header, payload;
 
 	private State state = State.BUFFER_EMPTY;
 	private int payloadLength = 0;
 
-	PacketReaderImpl(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
-			ObjectReader<SubscriptionUpdate> subscriptionUpdateReader,
-			InputStream in) {
+	PacketReaderImpl(CryptoComponent crypto, InputStream in) {
 		this.crypto = crypto;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.subscriptionUpdateReader = subscriptionUpdateReader;
 		this.in = in;
 		header = new byte[PACKET_HEADER_LENGTH];
 		payload = new byte[MAX_PACKET_PAYLOAD_LENGTH];
@@ -156,42 +142,4 @@ class PacketReaderImpl implements PacketReader {
 		if (!hasRequest()) throw new FormatException();
 		return new Request(Collections.unmodifiableList(readMessageIds()));
 	}
-
-	public boolean hasSubscriptionAck() throws IOException {
-		return !eof() && header[1] == SUBSCRIPTION_ACK;
-	}
-
-	public SubscriptionAck readSubscriptionAck() throws IOException {
-		if (!hasSubscriptionAck()) throw new FormatException();
-		// Set up the reader
-		InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength);
-		BdfReader r = bdfReaderFactory.createReader(bais);
-		// Read the start of the payload
-		r.readListStart();
-		// Read the version
-		long version = r.readInteger();
-		if (version < 0) throw new FormatException();
-		// Read the end of the payload
-		r.readListEnd();
-		if (!r.eof()) throw new FormatException();
-		state = State.BUFFER_EMPTY;
-		// Build and return the subscription ack
-		return new SubscriptionAck(version);
-	}
-
-	public boolean hasSubscriptionUpdate() throws IOException {
-		return !eof() && header[1] == SUBSCRIPTION_UPDATE;
-	}
-
-	public SubscriptionUpdate readSubscriptionUpdate() throws IOException {
-		if (!hasSubscriptionUpdate()) throw new FormatException();
-		// Set up the reader
-		InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength);
-		BdfReader r = bdfReaderFactory.createReader(bais);
-		// Read and build the subscription update
-		SubscriptionUpdate u = subscriptionUpdateReader.readObject(r);
-		if (!r.eof()) throw new FormatException();
-		state = State.BUFFER_EMPTY;
-		return u;
-	}
 }
diff --git a/briar-core/src/org/briarproject/sync/PacketWriterFactoryImpl.java b/briar-core/src/org/briarproject/sync/PacketWriterFactoryImpl.java
index 4c1b3fbb89660c943cc3a6f8dada5c62dcc37585..b5da5e653a1f279a115fff335bb61ae55a133b5a 100644
--- a/briar-core/src/org/briarproject/sync/PacketWriterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/sync/PacketWriterFactoryImpl.java
@@ -1,23 +1,13 @@
 package org.briarproject.sync;
 
-import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.sync.PacketWriter;
 import org.briarproject.api.sync.PacketWriterFactory;
 
 import java.io.OutputStream;
 
-import javax.inject.Inject;
-
 class PacketWriterFactoryImpl implements PacketWriterFactory {
 
-	private final BdfWriterFactory bdfWriterFactory;
-
-	@Inject
-	PacketWriterFactoryImpl(BdfWriterFactory bdfWriterFactory) {
-		this.bdfWriterFactory = bdfWriterFactory;
-	}
-
 	public PacketWriter createPacketWriter(OutputStream out) {
-		return new PacketWriterImpl(bdfWriterFactory, out);
+		return new PacketWriterImpl(out);
 	}
 }
diff --git a/briar-core/src/org/briarproject/sync/PacketWriterImpl.java b/briar-core/src/org/briarproject/sync/PacketWriterImpl.java
index 6ecc62eefd7607d2be9cb4c578720d1a36096fe8..f2e8817710e9d4147932c94cec9c4810995a17f0 100644
--- a/briar-core/src/org/briarproject/sync/PacketWriterImpl.java
+++ b/briar-core/src/org/briarproject/sync/PacketWriterImpl.java
@@ -1,17 +1,12 @@
 package org.briarproject.sync;
 
 import org.briarproject.api.UniqueId;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.sync.Ack;
-import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.PacketTypes;
 import org.briarproject.api.sync.PacketWriter;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.util.ByteUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -21,8 +16,6 @@ import java.io.OutputStream;
 import static org.briarproject.api.sync.PacketTypes.ACK;
 import static org.briarproject.api.sync.PacketTypes.OFFER;
 import static org.briarproject.api.sync.PacketTypes.REQUEST;
-import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_ACK;
-import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_UPDATE;
 import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.PACKET_HEADER_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.PROTOCOL_VERSION;
@@ -30,13 +23,11 @@ import static org.briarproject.api.sync.SyncConstants.PROTOCOL_VERSION;
 // This class is not thread-safe
 class PacketWriterImpl implements PacketWriter {
 
-	private final BdfWriterFactory bdfWriterFactory;
 	private final OutputStream out;
 	private final byte[] header;
 	private final ByteArrayOutputStream payload;
 
-	PacketWriterImpl(BdfWriterFactory bdfWriterFactory, OutputStream out) {
-		this.bdfWriterFactory = bdfWriterFactory;
+	PacketWriterImpl(OutputStream out) {
 		this.out = out;
 		header = new byte[PACKET_HEADER_LENGTH];
 		header[0] = PROTOCOL_VERSION;
@@ -94,33 +85,6 @@ class PacketWriterImpl implements PacketWriter {
 		writePacket(REQUEST);
 	}
 
-	public void writeSubscriptionAck(SubscriptionAck a) throws IOException {
-		if (payload.size() != 0) throw new IllegalStateException();
-		BdfWriter w = bdfWriterFactory.createWriter(payload);
-		w.writeListStart();
-		w.writeInteger(a.getVersion());
-		w.writeListEnd();
-		writePacket(SUBSCRIPTION_ACK);
-	}
-
-	public void writeSubscriptionUpdate(SubscriptionUpdate u)
-			throws IOException {
-		if (payload.size() != 0) throw new IllegalStateException();
-		BdfWriter w = bdfWriterFactory.createWriter(payload);
-		w.writeListStart();
-		w.writeListStart();
-		for (Group g : u.getGroups()) {
-			w.writeListStart();
-			w.writeRaw(g.getClientId().getBytes());
-			w.writeRaw(g.getDescriptor());
-			w.writeListEnd();
-		}
-		w.writeListEnd();
-		w.writeInteger(u.getVersion());
-		w.writeListEnd();
-		writePacket(SUBSCRIPTION_UPDATE);
-	}
-
 	public void flush() throws IOException {
 		out.flush();
 	}
diff --git a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
index 68e319abd262ec2ba892fd84c9755fc2577d96d5..0290b7d83503cd676ace8f8aaebb9d15875ae778 100644
--- a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
@@ -12,8 +12,6 @@ import org.briarproject.api.event.ShutdownEvent;
 import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.PacketWriter;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.SyncSession;
 
 import java.io.IOException;
@@ -66,16 +64,14 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 		this.transportId = transportId;
 		this.maxLatency = maxLatency;
 		this.packetWriter = packetWriter;
-		outstandingQueries = new AtomicInteger(4); // One per type of packet
+		outstandingQueries = new AtomicInteger(2); // One per type of packet
 		writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
 	}
 
 	public void run() throws IOException {
 		eventBus.addListener(this);
 		try {
-			// Start a query for each type of packet, in order of urgency
-			dbExecutor.execute(new GenerateSubscriptionAck());
-			dbExecutor.execute(new GenerateSubscriptionUpdate());
+			// Start a query for each type of packet
 			dbExecutor.execute(new GenerateAck());
 			dbExecutor.execute(new GenerateBatch());
 			// Write packets until interrupted or no more packets to write
@@ -187,77 +183,4 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 			dbExecutor.execute(new GenerateBatch());
 		}
 	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionAck implements Runnable {
-
-		public void run() {
-			if (interrupted) return;
-			try {
-				SubscriptionAck a = db.generateSubscriptionAck(contactId);
-				if (LOG.isLoggable(INFO))
-					LOG.info("Generated subscription ack: " + (a != null));
-				if (a == null) decrementOutstandingQueries();
-				else writerTasks.add(new WriteSubscriptionAck(a));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
-
-	// This tasks runs on the writer thread
-	private class WriteSubscriptionAck
-	implements ThrowingRunnable<IOException> {
-
-		private final SubscriptionAck ack;
-
-		private WriteSubscriptionAck(SubscriptionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() throws IOException {
-			if (interrupted) return;
-			packetWriter.writeSubscriptionAck(ack);
-			LOG.info("Sent subscription ack");
-			dbExecutor.execute(new GenerateSubscriptionAck());
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionUpdate implements Runnable {
-
-		public void run() {
-			if (interrupted) return;
-			try {
-				SubscriptionUpdate u =
-						db.generateSubscriptionUpdate(contactId, maxLatency);
-				if (LOG.isLoggable(INFO))
-					LOG.info("Generated subscription update: " + (u != null));
-				if (u == null) decrementOutstandingQueries();
-				else writerTasks.add(new WriteSubscriptionUpdate(u));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				interrupt();
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteSubscriptionUpdate
-	implements ThrowingRunnable<IOException> {
-
-		private final SubscriptionUpdate update;
-
-		private WriteSubscriptionUpdate(SubscriptionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() throws IOException {
-			if (interrupted) return;
-			packetWriter.writeSubscriptionUpdate(update);
-			LOG.info("Sent subscription update");
-			dbExecutor.execute(new GenerateSubscriptionUpdate());
-		}
-	}
 }
diff --git a/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java b/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java
deleted file mode 100644
index 38a297f214d4aa5938038ea2b4a3ad074f01eeb9..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.briarproject.sync;
-
-import org.briarproject.api.FormatException;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.ObjectReader;
-import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.SubscriptionUpdate;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS;
-
-class SubscriptionUpdateReader implements ObjectReader<SubscriptionUpdate> {
-
-	private final ObjectReader<Group> groupReader;
-
-	SubscriptionUpdateReader(ObjectReader<Group> groupReader) {
-		this.groupReader = groupReader;
-	}
-
-	public SubscriptionUpdate readObject(BdfReader r) throws IOException {
-		r.readListStart();
-		List<Group> groups = new ArrayList<Group>();
-		Set<GroupId> ids = new HashSet<GroupId>();
-		r.readListStart();
-		for (int i = 0; i < MAX_SUBSCRIPTIONS && !r.hasListEnd(); i++) {
-			Group g = groupReader.readObject(r);
-			if (!ids.add(g.getId())) throw new FormatException(); // Duplicate
-			groups.add(g);
-		}
-		r.readListEnd();
-		long version = r.readInteger();
-		if (version < 0) throw new FormatException();
-		r.readListEnd();
-		groups = Collections.unmodifiableList(groups);
-		return new SubscriptionUpdate(groups, version);
-	}
-}
diff --git a/briar-core/src/org/briarproject/sync/SyncModule.java b/briar-core/src/org/briarproject/sync/SyncModule.java
index 0b4a9a2c3b1bb8033d85925a00e2b64408fbc8a5..7a61c68a4e174f8848a270e8b7db9a4d7bbf18da 100644
--- a/briar-core/src/org/briarproject/sync/SyncModule.java
+++ b/briar-core/src/org/briarproject/sync/SyncModule.java
@@ -8,13 +8,11 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.PacketReaderFactory;
 import org.briarproject.api.sync.PacketWriterFactory;
 import org.briarproject.api.sync.PrivateGroupFactory;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.sync.SyncSessionFactory;
 import org.briarproject.api.sync.ValidationManager;
 
@@ -39,17 +37,6 @@ public class SyncModule extends AbstractModule {
 		return new AuthorReader(authorFactory);
 	}
 
-	@Provides
-	ObjectReader<Group> getGroupReader(GroupFactory groupFactory) {
-		return new GroupReader(groupFactory);
-	}
-
-	@Provides
-	ObjectReader<SubscriptionUpdate> getSubscriptionUpdateReader(
-			ObjectReader<Group> groupReader) {
-		return new SubscriptionUpdateReader(groupReader);
-	}
-
 	@Provides @Singleton
 	ValidationManager getValidationManager(LifecycleManager lifecycleManager,
 			EventBus eventBus, ValidationManagerImpl validationManager) {
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 7a5202c293ed6c98ce04068f4204f41d4401a7f9..5fca1458452be5ed09fc776e6de3d3f3b1c1f773 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -8,13 +8,14 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -84,7 +85,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
 					for (MessageId id : db.getMessagesToValidate(c)) {
 						try {
 							Message m = parseMessage(id, db.getRawMessage(id));
-							validateMessage(m, c);
+							Group g = db.getGroup(m.getGroupId());
+							validateMessage(m, g);
 						} catch (NoSuchMessageException e) {
 							LOG.info("Message removed before validation");
 						}
@@ -106,15 +108,15 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		return new Message(id, new GroupId(groupId), timestamp, raw);
 	}
 
-	private void validateMessage(final Message m, final ClientId c) {
+	private void validateMessage(final Message m, final Group g) {
 		cryptoExecutor.execute(new Runnable() {
 			public void run() {
-				MessageValidator v = validators.get(c);
+				MessageValidator v = validators.get(g.getClientId());
 				if (v == null) {
 					LOG.warning("No validator");
 				} else {
-					Metadata meta = v.validateMessage(m);
-					storeValidationResult(m, c, meta);
+					Metadata meta = v.validateMessage(m, g);
+					storeValidationResult(m, g.getClientId(), meta);
 				}
 			}
 		});
@@ -132,6 +134,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 							hook.validatingMessage(m, c, meta);
 						db.mergeMessageMetadata(m.getId(), meta);
 						db.setMessageValid(m, c, true);
+						db.setMessageShared(m, true);
 					}
 				} catch (NoSuchMessageException e) {
 					LOG.info("Message removed during validation");
@@ -146,19 +149,18 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	@Override
 	public void eventOccurred(Event e) {
 		if (e instanceof MessageAddedEvent) {
-			MessageAddedEvent m = (MessageAddedEvent) e;
 			// Validate the message if it wasn't created locally
-			if (m.getContactId() != null) loadClientId(m.getMessage());
+			MessageAddedEvent m = (MessageAddedEvent) e;
+			if (m.getContactId() != null) loadGroup(m.getMessage());
 		}
 	}
 
-	private void loadClientId(final Message m) {
+	private void loadGroup(final Message m) {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					ClientId c = db.getGroup(m.getGroupId()).getClientId();
-					validateMessage(m, c);
-				} catch (NoSuchSubscriptionException e) {
+					validateMessage(m, db.getGroup(m.getGroupId()));
+				} catch (NoSuchGroupException e) {
 					LOG.info("Group removed before validation");
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
diff --git a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
index 92f2d671751809b3f5c68c588a5c9f910208daf4..0b05dc0120adc42580da2e4d22f5072bfe97f5e9 100644
--- a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
+++ b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
@@ -2,7 +2,6 @@ package org.briarproject.plugins;
 
 import com.google.inject.Provides;
 
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.lifecycle.ShutdownManager;
 import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
@@ -16,6 +15,7 @@ import org.briarproject.plugins.modem.ModemPluginFactory;
 import org.briarproject.plugins.tcp.LanTcpPluginFactory;
 import org.briarproject.plugins.tcp.WanTcpPluginFactory;
 
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -39,10 +39,10 @@ public class DesktopPluginsModule extends PluginsModule {
 
 	@Provides
 	DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
-			CryptoComponent crypto, ReliabilityLayerFactory reliabilityFactory,
+			SecureRandom random, ReliabilityLayerFactory reliabilityFactory,
 			ShutdownManager shutdownManager) {
-		DuplexPluginFactory bluetooth = new BluetoothPluginFactory(
-				ioExecutor, crypto.getSecureRandom());
+		DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor,
+				random);
 		DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
 				reliabilityFactory);
 		DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor);
diff --git a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
index bcd2d4acb85402f78f8b2a4c454c497f88676439..e8c939d4cfe0811c6e71f3af8945e08dd8d52eed 100644
--- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
@@ -19,7 +19,6 @@ import org.briarproject.api.sync.PacketReaderFactory;
 import org.briarproject.api.sync.PacketWriter;
 import org.briarproject.api.sync.PacketWriterFactory;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
@@ -37,7 +36,6 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
@@ -56,7 +54,6 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 	private final ContactId contactId;
 	private final TransportId transportId;
 	private final SecretKey tagKey, headerKey;
-	private final Group group;
 	private final Message message, message1;
 	private final Collection<MessageId> messageIds;
 
@@ -79,7 +76,7 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		GroupFactory groupFactory = i.getInstance(GroupFactory.class);
 		ClientId clientId = new ClientId(TestUtils.getRandomId());
 		byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
-		group = groupFactory.createGroup(clientId, descriptor);
+		Group group = groupFactory.createGroup(clientId, descriptor);
 		// Add two messages to the group
 		MessageFactory messageFactory = i.getInstance(MessageFactory.class);
 		long timestamp = System.currentTimeMillis();
@@ -114,10 +111,6 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 
 		packetWriter.writeRequest(new Request(messageIds));
 
-		SubscriptionUpdate su = new SubscriptionUpdate(
-				Collections.singletonList(group), 1);
-		packetWriter.writeSubscriptionUpdate(su);
-
 		streamWriter.flush();
 		return out.toByteArray();
 	}
@@ -158,12 +151,6 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		Request req = packetReader.readRequest();
 		assertEquals(messageIds, req.getMessageIds());
 
-		// Read the subscription update
-		assertTrue(packetReader.hasSubscriptionUpdate());
-		SubscriptionUpdate su = packetReader.readSubscriptionUpdate();
-		assertEquals(Collections.singletonList(group), su.getGroups());
-		assertEquals(1, su.getVersion());
-
 		in.close();
 	}
 
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index abc95eda18a4abfe3813e63a9c3299fa9563ac1b..b0930805f3613372bff049863e98c2ac5bf43015 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -7,26 +7,26 @@ import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.MessageExistsException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchContactException;
+import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.db.NoSuchLocalAuthorException;
 import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.db.NoSuchSubscriptionException;
 import org.briarproject.api.db.NoSuchTransportException;
 import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
+import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
-import org.briarproject.api.event.SubscriptionAddedEvent;
-import org.briarproject.api.event.SubscriptionRemovedEvent;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
@@ -40,8 +40,6 @@ import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionAck;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.api.transport.IncomingKeys;
 import org.briarproject.api.transport.OutgoingKeys;
 import org.briarproject.api.transport.TransportKeys;
@@ -60,7 +58,6 @@ import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 public class DatabaseComponentImplTest extends BriarTestCase {
@@ -149,8 +146,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(false));
 			oneOf(database).addGroup(txn, group);
-			will(returnValue(true));
-			oneOf(eventBus).broadcast(with(any(SubscriptionAddedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(GroupAddedEvent.class)));
 			// addGroup() again
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
@@ -163,10 +159,9 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.emptyList()));
 			oneOf(database).removeGroup(txn, groupId);
+			oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(
-					SubscriptionRemovedEvent.class)));
-			oneOf(eventBus).broadcast(with(any(
-					LocalSubscriptionsUpdatedEvent.class)));
+					GroupVisibilityUpdatedEvent.class)));
 			// removeContact()
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -198,34 +193,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testDuplicateLocalMessagesAreNotStored() throws Exception {
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsMessage(txn, messageId);
-			will(returnValue(true));
-			oneOf(database).abortTransaction(txn);
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		try {
-			db.addLocalMessage(message, clientId, metadata, true);
-			fail();
-		} catch (MessageExistsException expected) {
-			// Expected
-		}
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLocalMessagesAreNotStoredUnlessSubscribed()
+	public void testLocalMessagesAreNotStoredUnlessGroupExists()
 			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
@@ -235,8 +203,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
-			oneOf(database).containsMessage(txn, messageId);
-			will(returnValue(false));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(false));
 			oneOf(database).abortTransaction(txn);
@@ -247,7 +213,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		try {
 			db.addLocalMessage(message, clientId, metadata, true);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
@@ -264,10 +230,10 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
-			oneOf(database).containsMessage(txn, messageId);
-			will(returnValue(false));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(false));
 			oneOf(database).addMessage(txn, message, VALID, true);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
 			oneOf(database).getVisibility(txn, groupId);
@@ -281,6 +247,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// The message was added, so the listeners should be called
 			oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
@@ -338,42 +305,42 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		}
 
 		try {
-			db.generateSubscriptionAck(contactId);
+			db.generateRequest(contactId, 123);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.generateSubscriptionUpdate(contactId, 123);
+			db.getContact(contactId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.getContact(contactId);
+			db.getMessageStatus(contactId, groupId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.getMessageStatus(contactId, groupId);
+			db.getMessageStatus(contactId, messageId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.getMessageStatus(contactId, messageId);
+			db.incrementStreamCounter(contactId, transportId, 0);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.incrementStreamCounter(contactId, transportId, 0);
+			db.isVisibleToContact(contactId, groupId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
@@ -403,31 +370,29 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		}
 
 		try {
-			SubscriptionAck a = new SubscriptionAck(0);
-			db.receiveSubscriptionAck(contactId, a);
+			Request r = new Request(Collections.singletonList(messageId));
+			db.receiveRequest(contactId, r);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			SubscriptionUpdate u = new SubscriptionUpdate(
-					Collections.<Group>emptyList(), 1);
-			db.receiveSubscriptionUpdate(contactId, u);
+			db.removeContact(contactId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.removeContact(contactId);
+			db.setReorderingWindow(contactId, transportId, 0, 0, new byte[4]);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
 		}
 
 		try {
-			db.setReorderingWindow(contactId, transportId, 0, 0, new byte[4]);
+			db.setVisibleToContact(contactId, groupId, true);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
@@ -480,7 +445,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testVariousMethodsThrowExceptionIfSubscriptionIsMissing()
+	public void testVariousMethodsThrowExceptionIfGroupIsMissing()
 			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
@@ -488,14 +453,15 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			// Check whether the subscription is in the DB (which it's not)
-			exactly(7).of(database).startTransaction();
+			// Check whether the group is in the DB (which it's not)
+			exactly(9).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(7).of(database).containsGroup(txn, groupId);
+			exactly(9).of(database).containsGroup(txn, groupId);
 			will(returnValue(false));
-			exactly(7).of(database).abortTransaction(txn);
-			// This is needed for getMessageStatus() to proceed
-			exactly(1).of(database).containsContact(txn, contactId);
+			exactly(9).of(database).abortTransaction(txn);
+			// This is needed for getMessageStatus(), isVisibleToContact(), and
+			// setVisibleToContact() to proceed
+			exactly(3).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -504,49 +470,63 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		try {
 			db.getGroup(groupId);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.getGroupMetadata(groupId);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.getMessageStatus(contactId, groupId);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.getVisibility(groupId);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
+			// Expected
+		}
+
+		try {
+			db.isVisibleToContact(contactId, groupId);
+			fail();
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.mergeGroupMetadata(groupId, metadata);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.removeGroup(group);
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
 		try {
 			db.setVisibility(groupId, Collections.<ContactId>emptyList());
 			fail();
-		} catch (NoSuchSubscriptionException expected) {
+		} catch (NoSuchGroupException expected) {
+			// Expected
+		}
+
+		try {
+			db.setVisibleToContact(contactId, groupId, true);
+			fail();
+		} catch (NoSuchGroupException expected) {
 			// Expected
 		}
 
@@ -856,58 +836,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
-	@Test
-	public void testGenerateSubscriptionUpdateNoUpdateDue() throws Exception {
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).getSubscriptionUpdate(txn, contactId, maxLatency);
-			will(returnValue(null));
-			oneOf(database).commitTransaction(txn);
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		assertNull(db.generateSubscriptionUpdate(contactId, maxLatency));
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testGenerateSubscriptionUpdate() throws Exception {
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).getSubscriptionUpdate(txn, contactId, maxLatency);
-			will(returnValue(new SubscriptionUpdate(
-					Collections.singletonList(group), 1)));
-			oneOf(database).commitTransaction(txn);
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		SubscriptionUpdate u = db.generateSubscriptionUpdate(contactId,
-				maxLatency);
-		assertEquals(Collections.singletonList(group), u.getGroups());
-		assertEquals(1, u.getVersion());
-
-		context.assertIsSatisfied();
-	}
-
 	@Test
 	public void testReceiveAck() throws Exception {
 		Mockery context = new Mockery();
@@ -950,7 +878,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
 			will(returnValue(true));
-			oneOf(database).addMessage(txn, message, UNKNOWN, true);
+			oneOf(database).addMessage(txn, message, UNKNOWN, false);
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
@@ -1103,56 +1031,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
-	@Test
-	public void testReceiveSubscriptionAck() throws Exception {
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).setSubscriptionUpdateAcked(txn, contactId, 1);
-			oneOf(database).commitTransaction(txn);
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		SubscriptionAck a = new SubscriptionAck(1);
-		db.receiveSubscriptionAck(contactId, a);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testReceiveSubscriptionUpdate() throws Exception {
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).setGroups(txn, contactId,
-					Collections.singletonList(group), 1);
-			oneOf(database).commitTransaction(txn);
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		SubscriptionUpdate u = new SubscriptionUpdate(
-				Collections.singletonList(group), 1);
-		db.receiveSubscriptionUpdate(contactId, u);
-
-		context.assertIsSatisfied();
-	}
-
 	@Test
 	public void testChangingVisibilityCallsListeners() throws Exception {
 		final ContactId contactId1 = new ContactId(123);
@@ -1172,10 +1050,9 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).getContactIds(txn);
 			will(returnValue(both));
 			oneOf(database).removeVisibility(txn, contactId1, groupId);
-			oneOf(database).setVisibleToAll(txn, groupId, false);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(
-					LocalSubscriptionsUpdatedEvent.class)));
+					GroupVisibilityUpdatedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
@@ -1204,7 +1081,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(both));
 			oneOf(database).getContactIds(txn);
 			will(returnValue(both));
-			oneOf(database).setVisibleToAll(txn, groupId, false);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -1215,55 +1091,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
-	@Test
-	public void testSettingVisibleToAllAffectsCurrentContacts()
-			throws Exception {
-		final ContactId contactId1 = new ContactId(123);
-		final Collection<ContactId> both = Arrays.asList(contactId, contactId1);
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			// setVisibility()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsGroup(txn, groupId);
-			will(returnValue(true));
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Collections.emptyList()));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
-			oneOf(database).addVisibility(txn, contactId, groupId);
-			oneOf(database).setVisibleToAll(txn, groupId, false);
-			oneOf(database).commitTransaction(txn);
-			oneOf(eventBus).broadcast(with(any(
-					LocalSubscriptionsUpdatedEvent.class)));
-			// setVisibleToAll()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsGroup(txn, groupId);
-			will(returnValue(true));
-			oneOf(database).setVisibleToAll(txn, groupId, true);
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
-			oneOf(database).addVisibility(txn, contactId1, groupId);
-			oneOf(database).commitTransaction(txn);
-			oneOf(eventBus).broadcast(with(any(
-					LocalSubscriptionsUpdatedEvent.class)));
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		db.setVisibility(groupId, Collections.singletonList(contactId));
-		db.setVisibleToAll(groupId, true);
-
-		context.assertIsSatisfied();
-	}
-
 	@Test
 	public void testTransportKeys() throws Exception {
 		final TransportKeys keys = createTransportKeys();
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index 3b127a88bcd7e527224f7b73adbed609d2df9758..2fe48c6c3e4f2240e5d00c09431581b400cc2882 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -62,7 +62,6 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	private final File testDir = TestUtils.getTestDirectory();
 	private final Random random = new Random();
-	private final ClientId clientId;
 	private final GroupId groupId;
 	private final Group group;
 	private final Author author;
@@ -77,8 +76,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	private final ContactId contactId;
 
 	public H2DatabaseTest() throws Exception {
-		clientId = new ClientId(TestUtils.getRandomId());
 		groupId = new GroupId(TestUtils.getRandomId());
+		ClientId clientId = new ClientId(TestUtils.getRandomId());
 		byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
 		group = new Group(groupId, clientId, descriptor);
 		AuthorId authorId = new AuthorId(TestUtils.getRandomId());
@@ -147,15 +146,15 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testUnsubscribingRemovesMessage() throws Exception {
+	public void testRemovingGroupRemovesMessage() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Subscribe to a group and store a message
+		// Add a group and a message
 		db.addGroup(txn, group);
 		db.addMessage(txn, message, VALID, true);
 
-		// Unsubscribing from the group should remove the message
+		// Removing the group should remove the message
 		assertTrue(db.containsMessage(txn, messageId));
 		db.removeGroup(txn, groupId);
 		assertFalse(db.containsMessage(txn, messageId));
@@ -169,12 +168,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store a message
+		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, true);
 
 		// The message has no status yet, so it should not be sendable
@@ -207,12 +205,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store an unvalidated message
+		// Add a contact, a group and an unvalidated message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, UNKNOWN, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -246,12 +243,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store an unshared message
+		// Add a contact, a group and an unshared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, false);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -280,55 +276,16 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testSendableMessagesMustBeSubscribed() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, subscribe to a group and store a message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The contact is not subscribed, so the message should not be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// The contact subscribing should make the message sendable
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// The contact unsubscribing should make the message unsendable
-		db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testSendableMessagesMustFitCapacity() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store a message
+		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -350,15 +307,14 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store a message
+		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
-		// The subscription is not visible to the contact, so the message
+		// The group is not visible to the contact, so the message
 		// should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
@@ -366,14 +322,14 @@ public class H2DatabaseTest extends BriarTestCase {
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertTrue(ids.isEmpty());
 
-		// Making the subscription visible should make the message sendable
+		// Making the group visible should make the message sendable
 		db.addVisibility(txn, contactId, groupId);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertEquals(Collections.singletonList(messageId), ids);
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertEquals(Collections.singletonList(messageId), ids);
 
-		// Making the subscription invisible should make the message unsendable
+		// Making the group invisible should make the message unsendable
 		db.removeVisibility(txn, contactId, groupId);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
@@ -389,11 +345,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and subscribe to a group
+		// Add a contact and a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
+		db.addVisibility(txn, contactId, groupId);
 
 		// Add some messages to ack
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
@@ -425,12 +381,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store a message
+		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -559,21 +514,20 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a transport to the database
-		db.addTransport(txn, transportId, 123);
-
-		// Set the transport config
+		// Store some settings
 		Settings s = new Settings();
 		s.put("foo", "foo");
 		s.put("bar", "bar");
 		db.mergeSettings(txn, s, "test");
 		assertEquals(s, db.getSettings(txn, "test"));
 
-		// Update one of the properties and add another
+		// Update one of the settings and add another
 		Settings s1 = new Settings();
 		s1.put("bar", "baz");
 		s1.put("bam", "bam");
 		db.mergeSettings(txn, s1, "test");
+
+		// Check that the settings were merged
 		Settings merged = new Settings();
 		merged.put("foo", "foo");
 		merged.put("bar", "baz");
@@ -590,12 +544,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and subscribe to a group
+		// Add a contact and a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// The message is not in the database
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -605,17 +558,16 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testContainsVisibleMessageRequiresLocalSubscription()
+	public void testContainsVisibleMessageRequiresGroupInDatabase()
 			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact with a subscription
+		// Add a contact
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
-		// There's no local subscription for the group
+		// The group is not in the database
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
 
 		db.commitTransaction(txn);
@@ -623,20 +575,19 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testContainsVisibleMessageRequiresVisibileSubscription()
+	public void testContainsVisibleMessageRequiresVisibileGroup()
 			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store a message
+		// Add a contact, a group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
-		// The subscription is not visible
+		// The group is not visible
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
 
 		db.commitTransaction(txn);
@@ -648,7 +599,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and subscribe to a group
+		// Add a contact and a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
@@ -670,7 +621,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testMultipleSubscriptionsAndUnsubscriptions() throws Exception {
+	public void testMultipleGroupChanges() throws Exception {
 		// Create some groups
 		List<Group> groups = new ArrayList<Group>();
 		for (int i = 0; i < 100; i++) {
@@ -683,7 +634,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and subscribe to the groups
+		// Add a contact and the groups
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		for (Group g : groups) db.addGroup(txn, g);
@@ -838,57 +789,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testGetAvailableGroups() throws Exception {
-		ContactId contactId1 = new ContactId(2);
-		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
-		Author author1 = new Author(authorId1, "Carol",
-				new byte[MAX_PUBLIC_KEY_LENGTH]);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add two contacts who subscribe to a group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		assertEquals(contactId1, db.addContact(txn, author1, localAuthorId));
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.setGroups(txn, contactId1, Collections.singletonList(group), 1);
-
-		// The group should be available
-		assertEquals(Collections.emptyList(), db.getGroups(txn, clientId));
-		assertEquals(Collections.singletonList(group),
-				db.getAvailableGroups(txn, clientId));
-
-		// Subscribe to the group - it should no longer be available
-		db.addGroup(txn, group);
-		assertEquals(Collections.singletonList(group),
-				db.getGroups(txn, clientId));
-		assertEquals(Collections.emptyList(),
-				db.getAvailableGroups(txn, clientId));
-
-		// Unsubscribe from the group - it should be available again
-		db.removeGroup(txn, groupId);
-		assertEquals(Collections.emptyList(), db.getGroups(txn, clientId));
-		assertEquals(Collections.singletonList(group),
-				db.getAvailableGroups(txn, clientId));
-
-		// The first contact unsubscribes - it should still be available
-		db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
-		assertEquals(Collections.emptyList(), db.getGroups(txn, clientId));
-		assertEquals(Collections.singletonList(group),
-				db.getAvailableGroups(txn, clientId));
-
-		// The second contact unsubscribes - it should no longer be available
-		db.setGroups(txn, contactId1, Collections.<Group>emptyList(), 2);
-		assertEquals(Collections.emptyList(), db.getGroups(txn, clientId));
-		assertEquals(Collections.emptyList(),
-				db.getAvailableGroups(txn, clientId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testGetContactsByLocalAuthorId() throws Exception {
 		Database<Connection> db = open(false);
@@ -943,46 +843,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testContactUnsubscribingResetsMessageStatus() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact who subscribes to a group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-
-		// Subscribe to the group and make it visible to the contact
-		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
-
-		// Add a message - it should be sendable to the contact
-		db.addMessage(txn, message, VALID, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-		Collection<MessageId> sendable = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), sendable);
-
-		// Mark the message as seen - it should no longer be sendable
-		db.raiseSeenFlag(txn, contactId, messageId);
-		sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.emptyList(), sendable);
-
-		// The contact unsubscribes - the message should not be sendable
-		db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
-		sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.emptyList(), sendable);
-
-		// The contact resubscribes - the message should be sendable again
-		db.setGroups(txn, contactId, Collections.singletonList(group), 3);
-		sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), sendable);
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testGroupMetadata() throws Exception {
 		Database<Connection> db = open(false);
@@ -1086,12 +946,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact who subscribes to a group
+		// Add a contact
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
-		// Subscribe to the group and make it visible to the contact
+		// Add a group and make it visible to the contact
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 
@@ -1158,6 +1017,31 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGroupsVisibleToContacts() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact and a group
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		db.addGroup(txn, group);
+
+		// The group should not be visible to the contact
+		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
+
+		// Make the group visible to the contact
+		db.addVisibility(txn, contactId, groupId);
+		assertTrue(db.containsVisibleGroup(txn, contactId, groupId));
+
+		// Make the group invisible to the contact
+		db.removeVisibility(txn, contactId, groupId);
+		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testDifferentLocalPseudonymsCanHaveTheSameContact()
 			throws Exception {
diff --git a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e10623d584a3e65ed4e662d17b43b4ac672569d2
--- /dev/null
+++ b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.forum;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class ForumListValidatorTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
diff --git a/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..07fc1ad80a77dacffcdf1093c7f09a954c1fffe1
--- /dev/null
+++ b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.forum;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class ForumSharingManagerImplTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
diff --git a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
index 4ec79282e5fe0d955b8e7ee32829ce1abdff1e6d..23fbddea8dc420ac9afa29677a0454b6c8ec8edf 100644
--- a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
@@ -83,7 +83,6 @@ public class PluginManagerImplTest extends BriarTestCase {
 			oneOf(simplexPlugin).getMaxLatency();
 			will(returnValue(simplexLatency));
 			oneOf(db).addTransport(simplexId, simplexLatency);
-			will(returnValue(true));
 			oneOf(simplexPlugin).start();
 			will(returnValue(true)); // Started
 			oneOf(simplexPlugin).shouldPoll();
@@ -98,7 +97,6 @@ public class PluginManagerImplTest extends BriarTestCase {
 			oneOf(simplexFailPlugin).getMaxLatency();
 			will(returnValue(simplexFailLatency));
 			oneOf(db).addTransport(simplexFailId, simplexFailLatency);
-			will(returnValue(true));
 			oneOf(simplexFailPlugin).start();
 			will(returnValue(false)); // Failed to start
 			// First duplex plugin
@@ -112,7 +110,6 @@ public class PluginManagerImplTest extends BriarTestCase {
 			oneOf(duplexPlugin).getMaxLatency();
 			will(returnValue(duplexLatency));
 			oneOf(db).addTransport(duplexId, duplexLatency);
-			will(returnValue(true));
 			oneOf(duplexPlugin).start();
 			will(returnValue(true)); // Started
 			oneOf(duplexPlugin).shouldPoll();
diff --git a/briar-tests/src/org/briarproject/sync/ConstantsTest.java b/briar-tests/src/org/briarproject/sync/ConstantsTest.java
index 4d0d498a4adcd5f3be7073ac41f9cb95af1c7504..ac3d4334f3fb7671d34afbeb6a7b1a33dbec14b6 100644
--- a/briar-tests/src/org/briarproject/sync/ConstantsTest.java
+++ b/briar-tests/src/org/briarproject/sync/ConstantsTest.java
@@ -22,16 +22,12 @@ import org.briarproject.api.messaging.MessagingConstants;
 import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.sync.Ack;
-import org.briarproject.api.sync.ClientId;
-import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.PacketWriter;
 import org.briarproject.api.sync.PacketWriterFactory;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.SubscriptionUpdate;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.data.DataModule;
@@ -52,9 +48,7 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENG
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS;
 import static org.junit.Assert.assertTrue;
 
 public class ConstantsTest extends BriarTestCase {
@@ -62,7 +56,6 @@ public class ConstantsTest extends BriarTestCase {
 	// TODO: Break this up into tests that are relevant for each package
 
 	private final CryptoComponent crypto;
-	private final GroupFactory groupFactory;
 	private final AuthorFactory authorFactory;
 	private final PrivateMessageFactory privateMessageFactory;
 	private final ForumPostFactory forumPostFactory;
@@ -75,7 +68,6 @@ public class ConstantsTest extends BriarTestCase {
 				new DataModule(), new EventModule(), new ForumModule(),
 				new IdentityModule(), new MessagingModule(), new SyncModule());
 		crypto = i.getInstance(CryptoComponent.class);
-		groupFactory = i.getInstance(GroupFactory.class);
 		authorFactory = i.getInstance(AuthorFactory.class);
 		privateMessageFactory = i.getInstance(PrivateMessageFactory.class);
 		forumPostFactory = i.getInstance(ForumPostFactory.class);
@@ -188,27 +180,6 @@ public class ConstantsTest extends BriarTestCase {
 		testMessageIdsFitIntoRequest(1000);
 	}
 
-	@Test
-	public void testGroupsFitIntoSubscriptionUpdate() throws Exception {
-		// Create the maximum number of maximum-length groups
-		Random random = new Random();
-		ClientId clientId = new ClientId(TestUtils.getRandomId());
-		Collection<Group> groups = new ArrayList<Group>();
-		for (int i = 0; i < MAX_SUBSCRIPTIONS; i++) {
-			byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
-			random.nextBytes(descriptor);
-			groups.add(groupFactory.createGroup(clientId, descriptor));
-		}
-		// Create a maximum-length subscription update
-		SubscriptionUpdate u = new SubscriptionUpdate(groups, Long.MAX_VALUE);
-		// Serialise the update
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketWriter writer = packetWriterFactory.createPacketWriter(out);
-		writer.writeSubscriptionUpdate(u);
-		// Check the size of the serialised subscription update
-		assertTrue(out.size() <= MAX_PACKET_PAYLOAD_LENGTH);
-	}
-
 	private void testMessageIdsFitIntoAck(int length) throws Exception {
 		// Create an ack with as many message IDs as possible
 		ByteArrayOutputStream out = new ByteArrayOutputStream(length);
diff --git a/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java b/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java
index 7224f5b6e28263f03d66a84c10d317af5d26531d..5bfc0971a289f2f7ca72fad708736e4f7a40b758 100644
--- a/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java
+++ b/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java
@@ -23,7 +23,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testFormatExceptionIfAckIsTooLarge() throws Exception {
 		byte[] b = createAck(true);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readAck();
 	}
 
@@ -31,7 +31,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
 		byte[] b = createAck(false);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readAck();
 	}
 
@@ -39,7 +39,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testEmptyAck() throws Exception {
 		byte[] b = createEmptyAck();
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readAck();
 	}
 
@@ -47,7 +47,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testFormatExceptionIfOfferIsTooLarge() throws Exception {
 		byte[] b = createOffer(true);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readOffer();
 	}
 
@@ -55,7 +55,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
 		byte[] b = createOffer(false);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readOffer();
 	}
 
@@ -63,7 +63,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testEmptyOffer() throws Exception {
 		byte[] b = createEmptyOffer();
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readOffer();
 	}
 
@@ -71,7 +71,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testFormatExceptionIfRequestIsTooLarge() throws Exception {
 		byte[] b = createRequest(true);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readRequest();
 	}
 
@@ -79,7 +79,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
 		byte[] b = createRequest(false);
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readRequest();
 	}
 
@@ -87,7 +87,7 @@ public class PacketReaderImplTest extends BriarTestCase {
 	public void testEmptyRequest() throws Exception {
 		byte[] b = createEmptyRequest();
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in);
+		PacketReaderImpl reader = new PacketReaderImpl(null, in);
 		reader.readRequest();
 	}
 
diff --git a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
index 983946f22e634a51c405c6dee458b42d79ea9618..ef2a58c476801cc4049f28ff149b92a178c00b0e 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
@@ -52,12 +52,6 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
-			// No subscription ack to send
-			oneOf(db).generateSubscriptionAck(contactId);
-			will(returnValue(null));
-			// No subscription update to send
-			oneOf(db).generateSubscriptionUpdate(contactId, maxLatency);
-			will(returnValue(null));
 			// No acks to send
 			oneOf(packetWriter).getMaxMessagesForAck(with(any(long.class)));
 			will(returnValue(MAX_MESSAGES_PER_ACK));
@@ -86,12 +80,6 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
-			// No subscription ack to send
-			oneOf(db).generateSubscriptionAck(contactId);
-			will(returnValue(null));
-			// No subscription update to send
-			oneOf(db).generateSubscriptionUpdate(contactId, maxLatency);
-			will(returnValue(null));
 			// One ack to send
 			oneOf(packetWriter).getMaxMessagesForAck(with(any(long.class)));
 			will(returnValue(MAX_MESSAGES_PER_ACK));