diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java
index 60a1ba3c3fa823f1cae110e01c28179bd7894d87..0339413b19383c76489dc9ebc69143b695f5de1c 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java
@@ -61,7 +61,7 @@ import static org.briarproject.api.sync.ValidationManager.State.PENDING;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-public class BlogManagerTest {
+public class BlogManagerTest extends BriarIntegrationTest {
 
 	private LifecycleManager lifecycleManager0, lifecycleManager1;
 	private SyncSessionFactory sync0, sync1;
@@ -94,7 +94,7 @@ public class BlogManagerTest {
 	private final String AUTHOR2 = "Author 2";
 
 	private static final Logger LOG =
-			Logger.getLogger(ForumSharingIntegrationTest.class.getName());
+			Logger.getLogger(BlogManagerTest.class.getName());
 
 	private BlogManagerTestComponent t0, t1;
 
diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..18a6d5eb438981b0dd3a3731e787a37c106fa5e1
--- /dev/null
+++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
@@ -0,0 +1,559 @@
+package org.briarproject;
+
+import net.jodah.concurrentunit.Waiter;
+
+import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.MessageStateChangedEvent;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.GroupMessage;
+import org.briarproject.api.privategroup.GroupMessageFactory;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.JoinMessageHeader;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.sync.SyncSession;
+import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.api.system.Clock;
+import org.briarproject.contact.ContactModule;
+import org.briarproject.crypto.CryptoModule;
+import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.privategroup.PrivateGroupModule;
+import org.briarproject.properties.PropertiesModule;
+import org.briarproject.sync.SyncModule;
+import org.briarproject.transport.TransportModule;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static org.briarproject.TestPluginsModule.MAX_LATENCY;
+import static org.briarproject.api.identity.Author.Status.VERIFIED;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class PrivateGroupManagerTest extends BriarIntegrationTest {
+
+	private LifecycleManager lifecycleManager0, lifecycleManager1;
+	private SyncSessionFactory sync0, sync1;
+	private PrivateGroupManager groupManager0, groupManager1;
+	private ContactManager contactManager0, contactManager1;
+	private ContactId contactId0, contactId1;
+	private IdentityManager identityManager0, identityManager1;
+	private LocalAuthor author0, author1;
+	private PrivateGroup privateGroup0;
+	private GroupId groupId0;
+	private GroupMessage newMemberMsg0;
+
+	@Inject
+	Clock clock;
+	@Inject
+	AuthorFactory authorFactory;
+	@Inject
+	CryptoComponent crypto;
+	@Inject
+	PrivateGroupFactory privateGroupFactory;
+	@Inject
+	GroupMessageFactory groupMessageFactory;
+
+	// objects accessed from background threads need to be volatile
+	private volatile Waiter validationWaiter;
+	private volatile Waiter deliveryWaiter;
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final SecretKey master = TestUtils.getSecretKey();
+	private final int TIMEOUT = 15000;
+	private final String AUTHOR1 = "Author 1";
+	private final String AUTHOR2 = "Author 2";
+
+	private static final Logger LOG =
+			Logger.getLogger(PrivateGroupManagerTest.class.getName());
+
+	private PrivateGroupManagerTestComponent t0, t1;
+
+	@Rule
+	public ExpectedException thrown = ExpectedException.none();
+
+	@Before
+	public void setUp() throws Exception {
+		PrivateGroupManagerTestComponent component =
+				DaggerPrivateGroupManagerTestComponent.builder().build();
+		component.inject(this);
+		injectEagerSingletons(component);
+
+		assertTrue(testDir.mkdirs());
+		File t0Dir = new File(testDir, AUTHOR1);
+		t0 = DaggerPrivateGroupManagerTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
+		injectEagerSingletons(t0);
+		File t1Dir = new File(testDir, AUTHOR2);
+		t1 = DaggerPrivateGroupManagerTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
+		injectEagerSingletons(t1);
+
+		identityManager0 = t0.getIdentityManager();
+		identityManager1 = t1.getIdentityManager();
+		contactManager0 = t0.getContactManager();
+		contactManager1 = t1.getContactManager();
+		groupManager0 = t0.getPrivateGroupManager();
+		groupManager1 = t1.getPrivateGroupManager();
+		sync0 = t0.getSyncSessionFactory();
+		sync1 = t1.getSyncSessionFactory();
+
+		// initialize waiters fresh for each test
+		validationWaiter = new Waiter();
+		deliveryWaiter = new Waiter();
+
+		startLifecycles();
+	}
+
+	@Test
+	public void testSendingMessage() throws Exception {
+		defaultInit();
+
+		// create and add test message
+		long time = clock.currentTimeMillis();
+		String body = "This is a test message!";
+		MessageId previousMsgId =
+				groupManager0.getPreviousMsgId(groupId0);
+		GroupMessage msg = groupMessageFactory
+				.createGroupMessage(groupId0, time, null, author0, body,
+						previousMsgId);
+		groupManager0.addLocalMessage(msg);
+		assertEquals(msg.getMessage().getId(),
+				groupManager0.getPreviousMsgId(groupId0));
+
+		// sync test message
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+
+		// assert that message arrived as expected
+		Collection<GroupMessageHeader> headers =
+				groupManager1.getHeaders(groupId0);
+		assertEquals(3, headers.size());
+		GroupMessageHeader header = null;
+		for (GroupMessageHeader h : headers) {
+			if (!(h instanceof JoinMessageHeader)) {
+				header = h;
+			}
+		}
+		assertTrue(header != null);
+		assertFalse(header.isRead());
+		assertEquals(author0, header.getAuthor());
+		assertEquals(time, header.getTimestamp());
+		assertEquals(VERIFIED, header.getAuthorStatus());
+		assertEquals(body, groupManager1.getMessageBody(header.getId()));
+		GroupCount count = groupManager1.getGroupCount(groupId0);
+		assertEquals(2, count.getUnreadCount());
+		assertEquals(time, count.getLatestMsgTime());
+		assertEquals(3, count.getMsgCount());
+	}
+
+	@Test
+	public void testMessageWithWrongPreviousMsgId() throws Exception {
+		defaultInit();
+
+		// create and add test message with no previousMsgId
+		GroupMessage msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
+						author0, "test", null);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with random previousMsgId
+		MessageId previousMsgId = new MessageId(TestUtils.getRandomId());
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
+						author0, "test", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with wrong previousMsgId
+		previousMsgId = groupManager1.getPreviousMsgId(groupId0);
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
+						author0, "test", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with previousMsgId of newMemberMsg
+		previousMsgId = newMemberMsg0.getMessage().getId();
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
+						author0, "test", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+	}
+
+	@Test
+	public void testMessageWithWrongParentMsgId() throws Exception {
+		defaultInit();
+
+		// create and add test message with random parentMsgId
+		MessageId parentMsgId = new MessageId(TestUtils.getRandomId());
+		MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
+		GroupMessage msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(),
+						parentMsgId, author0, "test", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with wrong parentMsgId
+		parentMsgId = previousMsgId;
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, clock.currentTimeMillis(),
+						parentMsgId, author0, "test", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+	}
+
+	@Test
+	public void testMessageWithWrongTimestamp() throws Exception {
+		defaultInit();
+
+		// create and add test message with wrong timestamp
+		MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
+		GroupMessage msg = groupMessageFactory
+				.createGroupMessage(groupId0, 42, null, author0, "test",
+						previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with good timestamp
+		long time = clock.currentTimeMillis();
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, time, null, author0, "test",
+						previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		assertEquals(3, groupManager1.getHeaders(groupId0).size());
+
+		// create and add test message with same timestamp as previous message
+		previousMsgId = msg.getMessage().getId();
+		msg = groupMessageFactory
+				.createGroupMessage(groupId0, time, previousMsgId, author0,
+						"test2", previousMsgId);
+		groupManager0.addLocalMessage(msg);
+
+		// sync test message
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that message did not arrive
+		assertEquals(3, groupManager1.getHeaders(groupId0).size());
+	}
+
+	@Test
+	public void testWrongJoinMessages() throws Exception {
+		addDefaultIdentities();
+		addDefaultContacts();
+		listenToEvents();
+
+		// author0 joins privateGroup0 with later timestamp
+		long joinTime = clock.currentTimeMillis();
+		GroupMessage newMemberMsg = groupMessageFactory
+				.createNewMemberMessage(groupId0, joinTime, author0, author0);
+		GroupMessage joinMsg = groupMessageFactory
+				.createJoinMessage(groupId0, joinTime + 1, author0,
+						newMemberMsg.getMessage().getId());
+		groupManager0.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
+		assertEquals(joinMsg.getMessage().getId(),
+				groupManager0.getPreviousMsgId(groupId0));
+
+		// make group visible to 1
+		Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
+		t0.getDatabaseComponent()
+				.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
+						true);
+		txn0.setComplete();
+		t0.getDatabaseComponent().endTransaction(txn0);
+
+		// author1 joins privateGroup0 and refers to wrong NEW_MEMBER message
+		joinMsg = groupMessageFactory
+				.createJoinMessage(groupId0, joinTime, author1,
+						newMemberMsg.getMessage().getId());
+		joinTime = clock.currentTimeMillis();
+		newMemberMsg = groupMessageFactory
+				.createNewMemberMessage(groupId0, joinTime, author0, author1);
+		groupManager1.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
+		assertEquals(joinMsg.getMessage().getId(),
+				groupManager1.getPreviousMsgId(groupId0));
+
+		// make group visible to 0
+		Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
+		t1.getDatabaseComponent()
+				.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
+						true);
+		txn1.setComplete();
+		t1.getDatabaseComponent().endTransaction(txn1);
+
+		// sync join messages
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that 0 never joined the group from 1's perspective
+		assertEquals(1, groupManager1.getHeaders(groupId0).size());
+
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 1);
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that 1 never joined the group from 0's perspective
+		assertEquals(1, groupManager0.getHeaders(groupId0).size());
+	}
+
+	@After
+	public void tearDown() throws Exception {
+		stopLifecycles();
+		TestUtils.deleteTestDirectory(testDir);
+	}
+
+	private class Listener implements EventListener {
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				if (!event.isLocal()) {
+					if (event.getState() == DELIVERED) {
+						LOG.info("Delivered new message");
+						deliveryWaiter.resume();
+					} else if (event.getState() == INVALID ||
+							event.getState() == PENDING) {
+						LOG.info("Validated new " + event.getState().name() +
+								" message");
+						validationWaiter.resume();
+					}
+				}
+			}
+		}
+	}
+
+	private void defaultInit() throws Exception {
+		addDefaultIdentities();
+		addDefaultContacts();
+		listenToEvents();
+		addGroup();
+	}
+
+	private void addDefaultIdentities() throws DbException {
+		KeyPair keyPair0 = crypto.generateSignatureKeyPair();
+		byte[] publicKey0 = keyPair0.getPublic().getEncoded();
+		byte[] privateKey0 = keyPair0.getPrivate().getEncoded();
+		author0 = authorFactory
+				.createLocalAuthor(AUTHOR1, publicKey0, privateKey0);
+		identityManager0.addLocalAuthor(author0);
+		privateGroup0 =
+				privateGroupFactory.createPrivateGroup("Testgroup", author0);
+		groupId0 = privateGroup0.getId();
+
+		KeyPair keyPair1 = crypto.generateSignatureKeyPair();
+		byte[] publicKey1 = keyPair1.getPublic().getEncoded();
+		byte[] privateKey1 = keyPair1.getPrivate().getEncoded();
+		author1 = authorFactory
+				.createLocalAuthor(AUTHOR2, publicKey1, privateKey1);
+		identityManager1.addLocalAuthor(author1);
+	}
+
+	private void addDefaultContacts() throws DbException {
+		// sharer adds invitee as contact
+		contactId1 = contactManager0.addContact(author1,
+				author0.getId(), master, clock.currentTimeMillis(), true,
+				true, true
+		);
+		// invitee adds sharer back
+		contactId0 = contactManager1.addContact(author0,
+				author1.getId(), master, clock.currentTimeMillis(), true,
+				true, true
+		);
+	}
+
+	private void listenToEvents() {
+		Listener listener0 = new Listener();
+		t0.getEventBus().addListener(listener0);
+		Listener listener1 = new Listener();
+		t1.getEventBus().addListener(listener1);
+	}
+
+	private void addGroup() throws Exception {
+		// author0 joins privateGroup0
+		long joinTime = clock.currentTimeMillis();
+		newMemberMsg0 = groupMessageFactory
+				.createNewMemberMessage(privateGroup0.getId(), joinTime,
+						author0, author0);
+		GroupMessage joinMsg = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author0,
+						newMemberMsg0.getMessage().getId());
+		groupManager0.addPrivateGroup(privateGroup0, newMemberMsg0, joinMsg);
+		assertEquals(joinMsg.getMessage().getId(),
+				groupManager0.getPreviousMsgId(groupId0));
+
+		// make group visible to 1
+		Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
+		t0.getDatabaseComponent()
+				.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
+						true);
+		txn0.setComplete();
+		t0.getDatabaseComponent().endTransaction(txn0);
+
+		// author1 joins privateGroup0
+		joinTime = clock.currentTimeMillis();
+		GroupMessage newMemberMsg1 = groupMessageFactory
+				.createNewMemberMessage(privateGroup0.getId(), joinTime,
+						author0, author1);
+		joinMsg = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
+						newMemberMsg1.getMessage().getId());
+		groupManager1.addPrivateGroup(privateGroup0, newMemberMsg1, joinMsg);
+		assertEquals(joinMsg.getMessage().getId(),
+				groupManager1.getPreviousMsgId(groupId0));
+
+		// make group visible to 0
+		Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
+		t1.getDatabaseComponent()
+				.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
+						true);
+		txn1.setComplete();
+		t1.getDatabaseComponent().endTransaction(txn1);
+
+		// sync join messages
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 2);
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 2);
+	}
+
+	private void sync0To1() throws IOException, TimeoutException {
+		deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
+	}
+
+	private void sync1To0() throws IOException, TimeoutException {
+		deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
+	}
+
+	private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
+			SyncSessionFactory toSync, ContactId toId, String debug)
+			throws IOException, TimeoutException {
+
+		if (debug != null) LOG.info("TEST: Sending message from " + debug);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		// Create an outgoing sync session
+		SyncSession sessionFrom =
+				fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
+		// Write whatever needs to be written
+		sessionFrom.run();
+		out.close();
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		// Create an incoming sync session
+		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
+		// Read whatever needs to be read
+		sessionTo.run();
+		in.close();
+	}
+
+	private void startLifecycles() throws InterruptedException {
+		// Start the lifecycle manager and wait for it to finish
+		lifecycleManager0 = t0.getLifecycleManager();
+		lifecycleManager1 = t1.getLifecycleManager();
+		lifecycleManager0.startServices();
+		lifecycleManager1.startServices();
+		lifecycleManager0.waitForStartup();
+		lifecycleManager1.waitForStartup();
+	}
+
+	private void stopLifecycles() throws InterruptedException {
+		// Clean up
+		lifecycleManager0.stopServices();
+		lifecycleManager1.stopServices();
+		lifecycleManager0.waitForShutdown();
+		lifecycleManager1.waitForShutdown();
+	}
+
+	private void injectEagerSingletons(
+			PrivateGroupManagerTestComponent component) {
+		component.inject(new LifecycleModule.EagerSingletons());
+		component.inject(new PrivateGroupModule.EagerSingletons());
+		component.inject(new CryptoModule.EagerSingletons());
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
+		component.inject(new PropertiesModule.EagerSingletons());
+	}
+
+}
diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..68191f4f87b7099a9723749fb5d12184a050ab6a
--- /dev/null
+++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java
@@ -0,0 +1,83 @@
+package org.briarproject;
+
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.clients.ClientsModule;
+import org.briarproject.contact.ContactModule;
+import org.briarproject.crypto.CryptoModule;
+import org.briarproject.data.DataModule;
+import org.briarproject.db.DatabaseModule;
+import org.briarproject.event.EventModule;
+import org.briarproject.identity.IdentityModule;
+import org.briarproject.lifecycle.LifecycleModule;
+import org.briarproject.messaging.MessagingModule;
+import org.briarproject.privategroup.PrivateGroupModule;
+import org.briarproject.properties.PropertiesModule;
+import org.briarproject.sharing.SharingModule;
+import org.briarproject.sync.SyncModule;
+import org.briarproject.system.SystemModule;
+import org.briarproject.transport.TransportModule;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+@Singleton
+@Component(modules = {
+		TestDatabaseModule.class,
+		TestPluginsModule.class,
+		TestSeedProviderModule.class,
+		ClientsModule.class,
+		ContactModule.class,
+		CryptoModule.class,
+		DataModule.class,
+		DatabaseModule.class,
+		EventModule.class,
+		MessagingModule.class,
+		PrivateGroupModule.class,
+		IdentityModule.class,
+		LifecycleModule.class,
+		PropertiesModule.class,
+		SharingModule.class,
+		SyncModule.class,
+		SystemModule.class,
+		TransportModule.class
+})
+interface PrivateGroupManagerTestComponent {
+
+	void inject(PrivateGroupManagerTest testCase);
+
+	void inject(ContactModule.EagerSingletons init);
+
+	void inject(CryptoModule.EagerSingletons init);
+
+	void inject(PrivateGroupModule.EagerSingletons init);
+
+	void inject(LifecycleModule.EagerSingletons init);
+
+	void inject(PropertiesModule.EagerSingletons init);
+
+	void inject(SyncModule.EagerSingletons init);
+
+	void inject(TransportModule.EagerSingletons init);
+
+	LifecycleManager getLifecycleManager();
+
+	EventBus getEventBus();
+
+	IdentityManager getIdentityManager();
+
+	ContactManager getContactManager();
+
+	PrivateGroupManager getPrivateGroupManager();
+
+	SyncSessionFactory getSyncSessionFactory();
+
+	DatabaseComponent getDatabaseComponent();
+
+}
diff --git a/briar-android/res/layout/list_item_forum_post.xml b/briar-android/res/layout/list_item_thread.xml
similarity index 98%
rename from briar-android/res/layout/list_item_forum_post.xml
rename to briar-android/res/layout/list_item_thread.xml
index 2fe00672178eab9dafc8e92dfd062626fec77260..f788c0ce8e6aff841c4d3f4dcae07e423576966c 100644
--- a/briar-android/res/layout/list_item_forum_post.xml
+++ b/briar-android/res/layout/list_item_thread.xml
@@ -1,12 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
+	android:id="@+id/layout"
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
-	android:id="@+id/forum_cell"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content"
-	android:orientation="horizontal">
+	android:orientation="horizontal"
+	android:baselineAligned="false">
 
 	<RelativeLayout
 		android:layout_width="wrap_content"
diff --git a/briar-android/res/layout/list_item_thread_notice.xml b/briar-android/res/layout/list_item_thread_notice.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2beecd1f2aabf4c2a2b209974e6d383268e3d8b1
--- /dev/null
+++ b/briar-android/res/layout/list_item_thread_notice.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	android:id="@+id/layout"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:layout_marginLeft="@dimen/margin_medium"
+	android:baselineAligned="false"
+	android:orientation="vertical">
+
+	<View
+		android:id="@+id/top_divider"
+		style="@style/Divider.ForumList"
+		android:layout_width="match_parent"
+		android:layout_height="@dimen/margin_separator"/>
+
+	<LinearLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginBottom="@dimen/margin_small"
+		android:layout_marginLeft="@dimen/margin_medium"
+		android:layout_marginRight="@dimen/margin_medium"
+		android:layout_marginTop="@dimen/margin_medium"
+		android:orientation="horizontal">
+
+		<org.briarproject.android.view.AuthorView
+			android:id="@+id/author"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			app:persona="commenter"/>
+
+		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
+			android:id="@+id/text"
+			android:layout_width="match_parent"
+			android:layout_height="match_parent"
+			android:layout_marginLeft="@dimen/margin_medium"
+			android:gravity="center_vertical"
+			android:textColor="@color/briar_text_secondary"
+			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="italic"
+			tools:text="@string/groups_member_joined"/>
+
+	</LinearLayout>
+
+</LinearLayout>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 878b281e48a5de9ec154e3b7b0ca69a2bf8467fe..132cea768dd5f12ffc8b3c4265d99fb5354eadeb 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -166,6 +166,7 @@
 	<string name="groups_invite_members">Invite Members</string>
 	<string name="groups_leave">Leave Group</string>
 	<string name="groups_dissolve">Dissolve Group</string>
+	<string name="groups_member_joined">joined the group.</string>
 
 	<!-- Private Group Invitations -->
 	<string name="groups_invitations_title">Group Invitations</string>
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 174a5c94668a193735716965d889c99644626b21..6550d1752b6b05fbd484da2f97d506bfd332fb91 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -120,6 +120,7 @@ public class ActivityModule {
 	@Provides
 	protected GroupController provideGroupController(
 			GroupControllerImpl groupController) {
+		activity.addLifecycleController(groupController);
 		return groupController;
 	}
 
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index 6081f1da9b5172bc1c1a6d2e7a74c47f37b6ad27..a436f9430fc9dc9d0e6b3582b8ed87f928160220 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -34,6 +34,8 @@ import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.plugins.ConnectionRegistry;
 import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.privategroup.GroupMessageFactory;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.settings.SettingsManager;
@@ -99,6 +101,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	GroupInvitationManager groupInvitationManager();
 
+	PrivateGroupFactory privateGroupFactory();
+
+	GroupMessageFactory groupMessageFactory();
+
 	ForumManager forumManager();
 
 	ForumSharingManager forumSharingManager();
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 307a7c5e158407d4a0cd173974607da181716181..36d552473a2684fbd3974a159c11d43d0a5c5ede 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -18,8 +18,9 @@ import android.widget.Toast;
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
-import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.ForumSharingStatusActivity;
+import org.briarproject.android.sharing.ShareForumActivity;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.briarproject.android.threaded.ThreadListActivity;
 import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
@@ -35,7 +36,7 @@ import static android.widget.Toast.LENGTH_SHORT;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
 
 public class ForumActivity extends
-		ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
+		ThreadListActivity<Forum, ForumItem, ForumPostHeader> {
 
 	private static final int REQUEST_FORUM_SHARED = 3;
 
@@ -74,9 +75,9 @@ public class ForumActivity extends
 	}
 
 	@Override
-	protected NestedForumAdapter createAdapter(
+	protected ThreadItemAdapter<ForumItem> createAdapter(
 			LinearLayoutManager layoutManager) {
-		return new NestedForumAdapter(this, layoutManager);
+		return new ThreadItemAdapter<>(this, layoutManager);
 	}
 
 	@Override
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index a0b89b0ed4aa4d9363bf20f4fb0487a3de5fc313..25d8328464377f8bff53ea5b33e018265db19538 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
 import android.support.annotation.Nullable;
 
 import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.android.threaded.ThreadListControllerImpl;
 import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.crypto.CryptoExecutor;
@@ -28,8 +29,11 @@ import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
-public class ForumControllerImpl extends
-		ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
+import static java.lang.Math.max;
+import static java.util.logging.Level.WARNING;
+
+public class ForumControllerImpl
+		extends ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
 		implements ForumController {
 
 	private static final Logger LOG =
@@ -42,9 +46,9 @@ public class ForumControllerImpl extends
 			LifecycleManager lifecycleManager, IdentityManager identityManager,
 			@CryptoExecutor Executor cryptoExecutor,
 			ForumManager forumManager, EventBus eventBus,
-			AndroidNotificationManager notificationManager, Clock clock) {
+			Clock clock, AndroidNotificationManager notificationManager) {
 		super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
-				eventBus, notificationManager, clock);
+				eventBus, clock, notificationManager);
 		this.forumManager = forumManager;
 	}
 
@@ -84,8 +88,8 @@ public class ForumControllerImpl extends
 	}
 
 	@Override
-	protected String loadMessageBody(MessageId id) throws DbException {
-		return StringUtils.fromUtf8(forumManager.getPostBody(id));
+	protected String loadMessageBody(ForumPostHeader h) throws DbException {
+		return StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
 	}
 
 	@Override
@@ -94,16 +98,42 @@ public class ForumControllerImpl extends
 	}
 
 	@Override
-	protected long getLatestTimestamp() throws DbException {
-		GroupCount count = forumManager.getGroupCount(getGroupId());
-		return count.getLatestMsgTime();
+	public void createAndStoreMessage(final String body,
+			@Nullable final ForumItem parentItem,
+			final ResultExceptionHandler<ForumItem, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					LocalAuthor author = identityManager.getLocalAuthor();
+					GroupCount count = forumManager.getGroupCount(getGroupId());
+					long timestamp = max(count.getLatestMsgTime() + 1,
+							clock.currentTimeMillis());
+					MessageId parentId = parentItem != null ?
+							parentItem.getId() : null;
+					createMessage(body, timestamp, parentId, author, handler);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
 	}
 
-	@Override
-	protected ForumPost createLocalMessage(String body, long timestamp,
-			@Nullable MessageId parentId, LocalAuthor author) {
-		return forumManager.createLocalPost(getGroupId(), body, timestamp,
-				parentId, author);
+	private void createMessage(final String body, final long timestamp,
+			final @Nullable MessageId parentId, final LocalAuthor author,
+			final ResultExceptionHandler<ForumItem, DbException> handler) {
+		cryptoExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Creating forum post...");
+				ForumPost msg = forumManager
+						.createLocalPost(getGroupId(), body, timestamp,
+								parentId, author);
+				storePost(msg, body, handler);
+			}
+		});
 	}
 
 	@Override
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
deleted file mode 100644
index 08d68b961301b4883a01e5c1f36ac6eb5ea83795..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.support.annotation.UiThread;
-import android.support.v7.widget.LinearLayoutManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.briarproject.R;
-import org.briarproject.android.threaded.ThreadItemAdapter;
-
-@UiThread
-class NestedForumAdapter extends ThreadItemAdapter<ForumItem> {
-
-	NestedForumAdapter(ThreadItemListener<ForumItem> listener,
-			LinearLayoutManager layoutManager) {
-		super(listener, layoutManager);
-	}
-
-	@Override
-	public NestedForumHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
-		View v = LayoutInflater.from(parent.getContext())
-				.inflate(R.layout.list_item_forum_post, parent, false);
-		return new NestedForumHolder(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
deleted file mode 100644
index b73558ff5d668c1adf37f0e8e69a9f971411967c..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.view.View;
-
-import org.briarproject.android.threaded.ThreadItemViewHolder;
-
-public class NestedForumHolder extends ThreadItemViewHolder<ForumItem> {
-
-	public NestedForumHolder(View v) {
-		super(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
index 71a4865b516bf1e14e8b5d1ef9c6317179092c2a..d17ed8d66374e1080c2761bce0a9f382a98c91ca 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
@@ -22,7 +22,7 @@ import javax.inject.Inject;
 import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
 
 public class GroupActivity extends
-		ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> {
+		ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
 
 	@Inject
 	GroupController controller;
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
index 8a6644efddaebff7b71a226c7631604e28b6f284..abbf10d423508e41b7459f74f389e48f9ea67b2d 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
@@ -3,8 +3,8 @@ package org.briarproject.android.privategroup.conversation;
 import android.support.annotation.Nullable;
 
 import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.android.threaded.ThreadListControllerImpl;
-import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
@@ -15,7 +15,9 @@ import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.privategroup.GroupMessage;
+import org.briarproject.api.privategroup.GroupMessageFactory;
 import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.JoinMessageHeader;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.sync.MessageId;
@@ -27,6 +29,9 @@ import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import static java.lang.Math.max;
+import static java.util.logging.Level.WARNING;
+
 public class GroupControllerImpl extends
 		ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage>
 		implements GroupController {
@@ -35,16 +40,19 @@ public class GroupControllerImpl extends
 			Logger.getLogger(GroupControllerImpl.class.getName());
 
 	private final PrivateGroupManager privateGroupManager;
+	private final GroupMessageFactory groupMessageFactory;
 
 	@Inject
 	GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, IdentityManager identityManager,
 			@CryptoExecutor Executor cryptoExecutor,
-			PrivateGroupManager privateGroupManager, EventBus eventBus,
-			AndroidNotificationManager notificationManager, Clock clock) {
+			PrivateGroupManager privateGroupManager,
+			GroupMessageFactory groupMessageFactory, EventBus eventBus,
+			Clock clock, AndroidNotificationManager notificationManager) {
 		super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
-				eventBus, notificationManager, clock);
+				eventBus, clock, notificationManager);
 		this.privateGroupManager = privateGroupManager;
+		this.groupMessageFactory = groupMessageFactory;
 	}
 
 	@Override
@@ -83,8 +91,13 @@ public class GroupControllerImpl extends
 	}
 
 	@Override
-	protected String loadMessageBody(MessageId id) throws DbException {
-		return privateGroupManager.getMessageBody(id);
+	protected String loadMessageBody(GroupMessageHeader header)
+			throws DbException {
+		if (header instanceof JoinMessageHeader) {
+			// will be looked up later
+			return "";
+		}
+		return privateGroupManager.getMessageBody(header.getId());
 	}
 
 	@Override
@@ -93,16 +106,52 @@ public class GroupControllerImpl extends
 	}
 
 	@Override
-	protected long getLatestTimestamp() throws DbException {
-		GroupCount count = privateGroupManager.getGroupCount(getGroupId());
-		return count.getLatestMsgTime();
+	public void createAndStoreMessage(final String body,
+			@Nullable final GroupMessageItem parentItem,
+			final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					LocalAuthor author = identityManager.getLocalAuthor();
+					MessageId parentId = null;
+					MessageId previousMsgId =
+							privateGroupManager.getPreviousMsgId(getGroupId());
+					// timestamp must be greater than the timestamps
+					// of the member's previous message...
+					long timestamp = privateGroupManager
+							.getMessageTimestamp(previousMsgId);
+					// ...and the parent post, if any
+					if (parentItem != null) {
+						timestamp = max(parentItem.getTimestamp(), timestamp);
+						parentId = parentItem.getId();
+					}
+					timestamp = max(clock.currentTimeMillis(), timestamp + 1);
+					createMessage(body, timestamp, parentId, author,
+							previousMsgId, handler);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
 	}
 
-	@Override
-	protected GroupMessage createLocalMessage(String body, long timestamp,
-			@Nullable MessageId parentId, LocalAuthor author) {
-		return privateGroupManager.createLocalMessage(getGroupId(), body,
-				timestamp, parentId, author);
+	private void createMessage(final String body, final long timestamp,
+			final @Nullable MessageId parentId, final LocalAuthor author,
+			final MessageId previousMsgId,
+			final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
+		cryptoExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Creating group message...");
+				GroupMessage msg = groupMessageFactory
+						.createGroupMessage(getGroupId(), timestamp,
+								parentId, author, body, previousMsgId);
+				storePost(msg, body, handler);
+			}
+		});
 	}
 
 	@Override
@@ -119,6 +168,9 @@ public class GroupControllerImpl extends
 	@Override
 	protected GroupMessageItem buildItem(GroupMessageHeader header,
 			String body) {
+		if (header instanceof JoinMessageHeader) {
+			return new JoinMessageItem(header, body);
+		}
 		return new GroupMessageItem(header, body);
 	}
 
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
index 19ee14adce62158808c7f795e9eb505ec889cd2f..c042a182965f3dbe067a3a5a343898c1f13cb0f1 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
@@ -1,5 +1,6 @@
 package org.briarproject.android.privategroup.conversation;
 
+import android.support.annotation.LayoutRes;
 import android.support.annotation.UiThread;
 import android.support.v7.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
@@ -7,7 +8,9 @@ import android.view.View;
 import android.view.ViewGroup;
 
 import org.briarproject.R;
+import org.briarproject.android.threaded.BaseThreadItemViewHolder;
 import org.briarproject.android.threaded.ThreadItemAdapter;
+import org.briarproject.android.threaded.ThreadPostViewHolder;
 
 @UiThread
 public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
@@ -17,12 +20,23 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
 		super(listener, layoutManager);
 	}
 
+	@LayoutRes
 	@Override
-	public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
+	public int getItemViewType(int position) {
+		GroupMessageItem item = getVisibleItem(position);
+		if (item != null) return item.getLayout();
+		return R.layout.list_item_thread;
+	}
+
+	@Override
+	public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
+			ViewGroup parent, int type) {
 		View v = LayoutInflater.from(parent.getContext())
-				.inflate(R.layout.list_item_forum_post, parent, false);
-		return new GroupMessageViewHolder(v);
+				.inflate(type, parent, false);
+		if (type == R.layout.list_item_thread_notice) {
+			return new JoinMessageItemViewHolder(v);
+		}
+		return new ThreadPostViewHolder<>(v);
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
index 7bde4a8bbcf183f06f275d7fbb0c49395259aea1..9deb0424e574fc582b56dc687246c3f74d3438fb 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
@@ -1,14 +1,22 @@
 package org.briarproject.android.privategroup.conversation;
 
+import android.support.annotation.LayoutRes;
+import android.support.annotation.UiThread;
+
+import org.briarproject.R;
 import org.briarproject.android.threaded.ThreadItem;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.privategroup.GroupMessageHeader;
 import org.briarproject.api.sync.MessageId;
 
+import javax.annotation.concurrent.NotThreadSafe;
+
+@UiThread
+@NotThreadSafe
 class GroupMessageItem extends ThreadItem {
 
-	GroupMessageItem(MessageId messageId, MessageId parentId,
+	private GroupMessageItem(MessageId messageId, MessageId parentId,
 			String text, long timestamp, Author author, Status status,
 			boolean isRead) {
 		super(messageId, parentId, text, timestamp, author, status, isRead);
@@ -19,4 +27,9 @@ class GroupMessageItem extends ThreadItem {
 				h.getAuthorStatus(), h.isRead());
 	}
 
+	@LayoutRes
+	public int getLayout() {
+		return R.layout.list_item_thread;
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
deleted file mode 100644
index 11825b8056e6ee3002338956a6599c2a4b949b21..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.briarproject.android.privategroup.conversation;
-
-import android.view.View;
-
-import org.briarproject.android.threaded.ThreadItemViewHolder;
-
-public class GroupMessageViewHolder
-		extends ThreadItemViewHolder<GroupMessageItem> {
-
-	public GroupMessageViewHolder(View v) {
-		super(v);
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..44c732ffd48970cc59a4abd2a92e4bebac232219
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItem.java
@@ -0,0 +1,35 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.support.annotation.LayoutRes;
+import android.support.annotation.UiThread;
+
+import org.briarproject.R;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@UiThread
+@NotThreadSafe
+class JoinMessageItem extends GroupMessageItem {
+
+	JoinMessageItem(GroupMessageHeader h,
+			String text) {
+		super(h, text);
+	}
+
+	@Override
+	public int getLevel() {
+		return 0;
+	}
+
+	@Override
+	public boolean hasDescendants() {
+		return false;
+	}
+
+	@LayoutRes
+	public int getLayout() {
+		return R.layout.list_item_thread_notice;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..972a6149eaa0d3093156c1df28bbe2c43fa77954
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/JoinMessageItemViewHolder.java
@@ -0,0 +1,30 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.BaseThreadItemViewHolder;
+import org.briarproject.android.threaded.ThreadItemAdapter;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+@UiThread
+@NotNullByDefault
+public class JoinMessageItemViewHolder
+		extends BaseThreadItemViewHolder<GroupMessageItem> {
+
+	public JoinMessageItemViewHolder(View v) {
+		super(v);
+	}
+
+	@Override
+	public void bind(final ThreadItemAdapter<GroupMessageItem> adapter,
+			final ThreadItemListener<GroupMessageItem> listener,
+			final GroupMessageItem item, int pos) {
+		super.bind(adapter, listener, item, pos);
+
+		textView.setText(getContext().getString(R.string.groups_member_joined));
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
index a35c2ac20435a7486ecbc1407989c3216d1b42e1..db661c43da5863f4b5a5805e7434ab88e2775e8b 100644
--- a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
@@ -3,11 +3,19 @@ package org.briarproject.android.privategroup.creation;
 import org.briarproject.android.controller.DbControllerImpl;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.GroupMessage;
+import org.briarproject.api.privategroup.GroupMessageFactory;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.system.Clock;
 
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -23,25 +31,81 @@ public class CreateGroupControllerImpl extends DbControllerImpl
 	private static final Logger LOG =
 			Logger.getLogger(CreateGroupControllerImpl.class.getName());
 
+	private final IdentityManager identityManager;
+	private final PrivateGroupFactory groupFactory;
+	private final GroupMessageFactory groupMessageFactory;
 	private final PrivateGroupManager groupManager;
+	private final Clock clock;
+	@CryptoExecutor
+	private final Executor cryptoExecutor;
 
 	@Inject
 	CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
-			LifecycleManager lifecycleManager,
-			PrivateGroupManager groupManager) {
+			@CryptoExecutor Executor cryptoExecutor,
+			LifecycleManager lifecycleManager, IdentityManager identityManager,
+			PrivateGroupFactory groupFactory,
+			GroupMessageFactory groupMessageFactory,
+			PrivateGroupManager groupManager, Clock clock) {
 		super(dbExecutor, lifecycleManager);
+		this.identityManager = identityManager;
+		this.groupFactory = groupFactory;
+		this.groupMessageFactory = groupMessageFactory;
 		this.groupManager = groupManager;
+		this.clock = clock;
+		this.cryptoExecutor = cryptoExecutor;
 	}
 
 	@Override
 	public void createGroup(final String name,
 			final ResultExceptionHandler<GroupId, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					LocalAuthor author = identityManager.getLocalAuthor();
+					createGroupAndMessages(author, name, handler);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	private void createGroupAndMessages(final LocalAuthor author,
+			final String name,
+			final ResultExceptionHandler<GroupId, DbException> handler) {
+		cryptoExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Creating group...");
+				PrivateGroup group =
+						groupFactory.createPrivateGroup(name, author);
+				LOG.info("Creating new member announcement...");
+				GroupMessage newMemberMsg = groupMessageFactory
+						.createNewMemberMessage(group.getId(),
+								clock.currentTimeMillis(), author, author);
+				LOG.info("Creating new join announcement...");
+				GroupMessage joinMsg = groupMessageFactory
+						.createJoinMessage(group.getId(),
+								newMemberMsg.getMessage().getTimestamp(),
+								author, newMemberMsg.getMessage().getId());
+				storeGroup(group, newMemberMsg, joinMsg, handler);
+			}
+		});
+	}
+
+	private void storeGroup(final PrivateGroup group,
+			final GroupMessage newMemberMsg, final GroupMessage joinMsg,
+			final ResultExceptionHandler<GroupId, DbException> handler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				LOG.info("Adding group to database...");
 				try {
-					handler.onResult(groupManager.addPrivateGroup(name));
+					groupManager.addPrivateGroup(group, newMemberMsg, joinMsg);
+					handler.onResult(group.getId());
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java
similarity index 56%
rename from briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
rename to briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java
index 0b5a5ddc5b74cc34bee5bb6d63b2534ce2aa0227..af8e800b33ed2d308198ffc58a914279a10abea2 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
+++ b/briar-android/src/org/briarproject/android/threaded/BaseThreadItemViewHolder.java
@@ -5,6 +5,7 @@ import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.drawable.ColorDrawable;
+import android.support.annotation.CallSuper;
 import android.support.annotation.UiThread;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.widget.RecyclerView;
@@ -15,47 +16,32 @@ import android.widget.TextView;
 import org.briarproject.R;
 import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
 import org.briarproject.android.view.AuthorView;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.util.StringUtils;
 
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
 @UiThread
-public abstract class ThreadItemViewHolder<I extends ThreadItem>
+@NotNullByDefault
+public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
 		extends RecyclerView.ViewHolder {
 
 	private final static int ANIMATION_DURATION = 5000;
 
-	private final TextView textView, lvlText, repliesText;
+	protected final TextView textView;
+	private final ViewGroup layout;
 	private final AuthorView author;
-	private final View[] lvls;
-	private final View chevron, replyButton;
-	private final ViewGroup cell;
 	private final View topDivider;
 
-	public ThreadItemViewHolder(View v) {
+	public BaseThreadItemViewHolder(View v) {
 		super(v);
 
+		layout = (ViewGroup) v.findViewById(R.id.layout);
 		textView = (TextView) v.findViewById(R.id.text);
-		lvlText = (TextView) v.findViewById(R.id.nested_line_text);
 		author = (AuthorView) v.findViewById(R.id.author);
-		repliesText = (TextView) v.findViewById(R.id.replies);
-		int[] nestedLineIds = {
-				R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
-				R.id.nested_line_4, R.id.nested_line_5
-		};
-		lvls = new View[nestedLineIds.length];
-		for (int i = 0; i < lvls.length; i++) {
-			lvls[i] = v.findViewById(nestedLineIds[i]);
-		}
-		chevron = v.findViewById(R.id.chevron);
-		replyButton = v.findViewById(R.id.btn_reply);
-		cell = (ViewGroup) v.findViewById(R.id.forum_cell);
 		topDivider = v.findViewById(R.id.top_divider);
 	}
 
 	// TODO improve encapsulation, so we don't need to pass the adapter here
+	@CallSuper
 	public void bind(final ThreadItemAdapter<I> adapter,
 			final ThreadItemListener<I> listener, final I item, int pos) {
 
@@ -67,68 +53,22 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 			topDivider.setVisibility(View.VISIBLE);
 		}
 
-		for (int i = 0; i < lvls.length; i++) {
-			lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
-		}
-		if (item.getLevel() > 5) {
-			lvlText.setVisibility(VISIBLE);
-			lvlText.setText("" + item.getLevel());
-		} else {
-			lvlText.setVisibility(GONE);
-		}
 		author.setAuthor(item.getAuthor());
 		author.setDate(item.getTimestamp());
 		author.setAuthorStatus(item.getStatus());
 
-		int replies = adapter.getReplyCount(item);
-		if (replies == 0) {
-			repliesText.setText("");
-		} else {
-			repliesText.setText(getContext().getResources()
-					.getQuantityString(R.plurals.message_replies, replies,
-							replies));
-		}
-
-		if (item.hasDescendants()) {
-			chevron.setVisibility(VISIBLE);
-			if (item.isShowingDescendants()) {
-				chevron.setSelected(false);
-			} else {
-				chevron.setSelected(true);
-			}
-			chevron.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					chevron.setSelected(!chevron.isSelected());
-					if (chevron.isSelected()) {
-						adapter.hideDescendants(item);
-					} else {
-						adapter.showDescendants(item);
-					}
-				}
-			});
-		} else {
-			chevron.setVisibility(INVISIBLE);
-		}
 		if (item.equals(adapter.getReplyItem())) {
-			cell.setBackgroundColor(ContextCompat
+			layout.setBackgroundColor(ContextCompat
 					.getColor(getContext(), R.color.forum_cell_highlight));
 		} else if (item.equals(adapter.getAddedItem())) {
-			cell.setBackgroundColor(ContextCompat
+			layout.setBackgroundColor(ContextCompat
 					.getColor(getContext(), R.color.forum_cell_highlight));
 			animateFadeOut(adapter, adapter.getAddedItem());
 			adapter.clearAddedItem();
 		} else {
-			cell.setBackgroundColor(ContextCompat
+			layout.setBackgroundColor(ContextCompat
 					.getColor(getContext(), R.color.window_background));
 		}
-		replyButton.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onReplyClick(item);
-				adapter.scrollTo(item);
-			}
-		});
 	}
 
 	private void animateFadeOut(final ThreadItemAdapter<I> adapter,
@@ -137,7 +77,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		setIsRecyclable(false);
 		ValueAnimator anim = new ValueAnimator();
 		adapter.addAnimatingItem(addedItem, anim);
-		ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
+		ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
 		anim.setIntValues(viewColor.getColor(), ContextCompat
 				.getColor(getContext(), R.color.window_background));
 		anim.setEvaluator(new ArgbEvaluator());
@@ -167,7 +107,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
 			@Override
 			public void onAnimationUpdate(ValueAnimator valueAnimator) {
-				cell.setBackgroundColor(
+				layout.setBackgroundColor(
 						(Integer) valueAnimator.getAnimatedValue());
 			}
 		});
@@ -175,7 +115,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
 		anim.start();
 	}
 
-	private Context getContext() {
+	protected Context getContext() {
 		return textView.getContext();
 	}
 
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
index a5a22075995246e5859671bc7b64995324bb75ea..e4c055e2b39a31c228ff72c77964d76dd48a5ce2 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
@@ -5,9 +5,11 @@ import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.sync.MessageId;
 
+import javax.annotation.concurrent.NotThreadSafe;
+
 import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
 
-/* This class is not thread safe */
+@NotThreadSafe
 public abstract class ThreadItem implements MessageNode {
 
 	private final MessageId messageId;
@@ -92,4 +94,5 @@ public abstract class ThreadItem implements MessageNode {
 	public void setDescendantCount(int descendantCount) {
 		this.descendantCount = descendantCount;
 	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
index c8ab7f2ba0bf173f8688939167c40db894683663..7c3ee9079128f1d1424ab570e78f1da3c3b126e4 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
@@ -5,7 +5,11 @@ import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 
+import org.briarproject.R;
 import org.briarproject.android.util.VersionedAdapter;
 import org.briarproject.api.sync.MessageId;
 
@@ -17,8 +21,8 @@ import java.util.Map;
 
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
 
-public abstract class ThreadItemAdapter<I extends ThreadItem>
-		extends RecyclerView.Adapter<ThreadItemViewHolder<I>>
+public class ThreadItemAdapter<I extends ThreadItem>
+		extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
 		implements VersionedAdapter {
 
 	static final int UNDEFINED = -1;
@@ -42,7 +46,15 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
 	}
 
 	@Override
-	public void onBindViewHolder(ThreadItemViewHolder<I> ui, int position) {
+	public BaseThreadItemViewHolder<I> onCreateViewHolder(
+			ViewGroup parent, int viewType) {
+		View v = LayoutInflater.from(parent.getContext())
+				.inflate(R.layout.list_item_thread, parent, false);
+		return new ThreadPostViewHolder<>(v);
+	}
+
+	@Override
+	public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
 		I item = getVisibleItem(position);
 		if (item == null) return;
 		listener.onItemVisible(item);
@@ -304,7 +316,7 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
 		revision++;
 	}
 
-	protected interface ThreadItemListener<I> {
+	public interface ThreadItemListener<I> {
 
 		void onItemVisible(I item);
 
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
index a8eefd250f474069e20a40fb458af1013c43976f..eff17e175e13264ea28ab2381ae422d87e2e0aef 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
@@ -35,7 +35,7 @@ import static android.support.design.widget.Snackbar.make;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
-public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
+public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
 		extends BriarActivity
 		implements ThreadListListener<H>, TextInputListener,
 		ThreadItemListener<I> {
@@ -46,7 +46,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
 	private static final Logger LOG =
 			Logger.getLogger(ThreadListActivity.class.getName());
 
-	protected A adapter;
+	protected ThreadItemAdapter<I> adapter;
 	protected BriarRecyclerView list;
 	protected TextInputView textInput;
 	protected GroupId groupId;
@@ -88,7 +88,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
 	@LayoutRes
 	protected abstract int getLayout();
 
-	protected abstract A createAdapter(LinearLayoutManager layoutManager);
+	protected abstract ThreadItemAdapter<I> createAdapter(
+			LinearLayoutManager layoutManager);
 
 	protected void loadNamedGroup() {
 		getController().loadNamedGroup(
@@ -249,8 +250,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
 						finish();
 					}
 				};
-		getController().createAndStoreMessage(text,
-				replyItem != null ? replyItem.getId() : null, handler);
+		getController().createAndStoreMessage(text, replyItem, handler);
 		textInput.hideSoftKeyboard();
 		textInput.setVisibility(GONE);
 		textInput.setText("");
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
index a7731d8d48ed2046f39c962e8781b86c4c9c396d..f2e7570a820d89f66cf7e3452d74b3a1a4256514 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
@@ -10,7 +10,6 @@ import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
 
 import java.util.Collection;
 
@@ -29,7 +28,7 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
 
 	void markItemsRead(Collection<I> items);
 
-	void createAndStoreMessage(String body, @Nullable MessageId parentId,
+	void createAndStoreMessage(String body, @Nullable I parentItem,
 			ResultExceptionHandler<I, DbException> handler);
 
 	void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
index 80a5b44ea2ab4a968dc897dcd5d6c6f517c7b84a..6ea72c892357f669bc1fcd8d322ddca3b4b787cc 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
@@ -2,7 +2,6 @@ package org.briarproject.android.threaded;
 
 import android.app.Activity;
 import android.support.annotation.CallSuper;
-import android.support.annotation.Nullable;
 
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.controller.DbControllerImpl;
@@ -18,7 +17,6 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.identity.IdentityManager;
-import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
@@ -43,28 +41,28 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 	private static final Logger LOG =
 			Logger.getLogger(ThreadListControllerImpl.class.getName());
 
-	private final IdentityManager identityManager;
-	private final Executor cryptoExecutor;
+	protected final IdentityManager identityManager;
+	protected final Executor cryptoExecutor;
 	protected final AndroidNotificationManager notificationManager;
+	protected final Clock clock;
 	private final EventBus eventBus;
-	private final Clock clock;
 
 	private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
 
 	private volatile GroupId groupId;
 
-	protected ThreadListListener<H> listener;
+	protected volatile ThreadListListener<H> listener;
 
 	protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, IdentityManager identityManager,
 			@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
-			AndroidNotificationManager notificationManager, Clock clock) {
+			Clock clock, AndroidNotificationManager notificationManager) {
 		super(dbExecutor, lifecycleManager);
 		this.identityManager = identityManager;
 		this.cryptoExecutor = cryptoExecutor;
-		this.eventBus = eventBus;
 		this.notificationManager = notificationManager;
 		this.clock = clock;
+		this.eventBus = eventBus;
 	}
 
 	@Override
@@ -160,7 +158,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 					for (H header : headers) {
 						if (!bodyCache.containsKey(header.getId())) {
 							bodyCache.put(header.getId(),
-									loadMessageBody(header.getId()));
+									loadMessageBody(header));
 						}
 					}
 					duration = System.currentTimeMillis() - now;
@@ -182,7 +180,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 	protected abstract Collection<H> loadHeaders() throws DbException;
 
 	@DatabaseExecutor
-	protected abstract String loadMessageBody(MessageId id) throws DbException;
+	protected abstract String loadMessageBody(H header) throws DbException;
 
 	@Override
 	public void loadItem(final H header,
@@ -194,7 +192,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 					long now = System.currentTimeMillis();
 					String body;
 					if (!bodyCache.containsKey(header.getId())) {
-						body = loadMessageBody(header.getId());
+						body = loadMessageBody(header);
 						bodyCache.put(header.getId(), body);
 					} else {
 						body = bodyCache.get(header.getId());
@@ -242,57 +240,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
 	@DatabaseExecutor
 	protected abstract void markRead(MessageId id) throws DbException;
 
-	@Override
-	public void createAndStoreMessage(final String body,
-			@Nullable final MessageId parentId,
-			final ResultExceptionHandler<I, DbException> handler) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					LocalAuthor author = identityManager.getLocalAuthor();
-					long timestamp = getLatestTimestamp();
-					timestamp = Math.max(timestamp, clock.currentTimeMillis());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO)) {
-						LOG.info("Loading identity and timestamp took " +
-								duration + " ms");
-					}
-					createMessage(body, timestamp, parentId, author, handler);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					handler.onException(e);
-				}
-			}
-		});
-	}
-
-	@DatabaseExecutor
-	protected abstract long getLatestTimestamp() throws DbException;
-
-	private void createMessage(final String body, final long timestamp,
-			final @Nullable MessageId parentId, final LocalAuthor author,
-			final ResultExceptionHandler<I, DbException> handler) {
-		cryptoExecutor.execute(new Runnable() {
-			@Override
-			public void run() {
-				long now = System.currentTimeMillis();
-				M msg = createLocalMessage(body, timestamp, parentId, author);
-				long duration = System.currentTimeMillis() - now;
-				if (LOG.isLoggable(INFO))
-					LOG.info("Creating message took " + duration + " ms");
-				storePost(msg, body, handler);
-			}
-		});
-	}
-
-	@CryptoExecutor
-	protected abstract M createLocalMessage(String body, long timestamp,
-			@Nullable MessageId parentId, LocalAuthor author);
-
-	private void storePost(final M msg, final String body,
+	protected void storePost(final M msg, final String body,
 			final ResultExceptionHandler<I, DbException> resultHandler) {
 		runOnDbThread(new Runnable() {
 			@Override
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java b/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..bffd1d6eaeb5a73f6b30ebedc617a37ded04b06c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadPostViewHolder.java
@@ -0,0 +1,96 @@
+package org.briarproject.android.threaded;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+@NotNullByDefault
+public class ThreadPostViewHolder<I extends ThreadItem>
+		extends BaseThreadItemViewHolder<I> {
+
+	private final TextView  lvlText, repliesText;
+	private final View[] lvls;
+	private final View chevron, replyButton;
+
+	public ThreadPostViewHolder(View v) {
+		super(v);
+
+		lvlText = (TextView) v.findViewById(R.id.nested_line_text);
+		repliesText = (TextView) v.findViewById(R.id.replies);
+		int[] nestedLineIds = {
+				R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
+				R.id.nested_line_4, R.id.nested_line_5
+		};
+		lvls = new View[nestedLineIds.length];
+		for (int i = 0; i < lvls.length; i++) {
+			lvls[i] = v.findViewById(nestedLineIds[i]);
+		}
+		chevron = v.findViewById(R.id.chevron);
+		replyButton = v.findViewById(R.id.btn_reply);
+	}
+
+	// TODO improve encapsulation, so we don't need to pass the adapter here
+	@Override
+	public void bind(final ThreadItemAdapter<I> adapter,
+			final ThreadItemListener<I> listener, final I item, int pos) {
+		super.bind(adapter, listener, item, pos);
+
+		for (int i = 0; i < lvls.length; i++) {
+			lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
+		}
+		if (item.getLevel() > 5) {
+			lvlText.setVisibility(VISIBLE);
+			lvlText.setText("" + item.getLevel());
+		} else {
+			lvlText.setVisibility(GONE);
+		}
+
+		int replies = adapter.getReplyCount(item);
+		if (replies == 0) {
+			repliesText.setText("");
+		} else {
+			repliesText.setText(getContext().getResources()
+					.getQuantityString(R.plurals.message_replies, replies,
+							replies));
+		}
+
+		if (item.hasDescendants()) {
+			chevron.setVisibility(VISIBLE);
+			if (item.isShowingDescendants()) {
+				chevron.setSelected(false);
+			} else {
+				chevron.setSelected(true);
+			}
+			chevron.setOnClickListener(new View.OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					chevron.setSelected(!chevron.isSelected());
+					if (chevron.isSelected()) {
+						adapter.hideDescendants(item);
+					} else {
+						adapter.showDescendants(item);
+					}
+				}
+			});
+		} else {
+			chevron.setVisibility(INVISIBLE);
+		}
+		replyButton.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				listener.onReplyClick(item);
+				adapter.scrollTo(item);
+			}
+		});
+	}
+
+}
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 991f59f0f26dca492ff6b7204871430f0c0d4928..cedacc77d08850c7dc891bd4647f215150bb9f5f 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -8,6 +8,7 @@ import org.briarproject.BuildConfig;
 import org.briarproject.TestUtils;
 import org.briarproject.android.TestBriarApplication;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
@@ -111,7 +112,7 @@ public class ForumActivityTest {
 		List<ForumItem> dummyData = getDummyData();
 		verify(mc, times(1)).loadItems(rc.capture());
 		rc.getValue().onResult(dummyData);
-		NestedForumAdapter adapter = forumActivity.getAdapter();
+		ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
 		Assert.assertNotNull(adapter);
 		// Cascade close
 		assertEquals(6, adapter.getItemCount());
diff --git a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
index 806fee299bf73a5bb23a034dfd8541765b1b29d3..a77a88cc156c4771d0da6689eca875539f624037 100644
--- a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
+++ b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
 import org.briarproject.android.ActivityModule;
 import org.briarproject.android.controller.BriarController;
 import org.briarproject.android.controller.BriarControllerImpl;
+import org.briarproject.android.threaded.ThreadItemAdapter;
 import org.mockito.Mockito;
 
 /**
@@ -15,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
 		return forumController;
 	}
 
-	public NestedForumAdapter getAdapter() {
+	public ThreadItemAdapter<ForumItem> getAdapter() {
 		return adapter;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/clients/BaseMessage.java b/briar-api/src/org/briarproject/api/clients/BaseMessage.java
index fbfb56337789d1e2cb3dbee512fc5e170b04b84e..ee3c0319f484e0d58889681ed1acd20f30b24d4c 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseMessage.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseMessage.java
@@ -3,7 +3,6 @@ package org.briarproject.api.clients;
 import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import javax.annotation.concurrent.Immutable;
diff --git a/briar-api/src/org/briarproject/api/clients/ClientHelper.java b/briar-api/src/org/briarproject/api/clients/ClientHelper.java
index 5aeffaa21ec026a5708cb281bcdf721206054a89..b24c9d2b715456a4137bef4246209bdc5853a741 100644
--- a/briar-api/src/org/briarproject/api/clients/ClientHelper.java
+++ b/briar-api/src/org/briarproject/api/clients/ClientHelper.java
@@ -6,6 +6,7 @@ import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 
@@ -81,4 +82,8 @@ public interface ClientHelper {
 
 	byte[] sign(BdfList toSign, byte[] privateKey)
 			throws FormatException, GeneralSecurityException;
+
+	void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
+			throws FormatException, GeneralSecurityException;
+
 }
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
index 06f277460e7828d4231bbefc58b4a127e9b7be2c..4de6bbbb0a0430fba36ef09ddf1c2dd0ac610958 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
@@ -5,7 +5,6 @@ import org.briarproject.api.identity.Author;
 import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import javax.annotation.concurrent.Immutable;
@@ -14,16 +13,16 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class GroupMessage extends BaseMessage {
 
-	private final Author author;
+	private final Author member;
 
 	public GroupMessage(Message message, @Nullable MessageId parent,
-			Author author) {
+			Author member) {
 		super(message, parent);
-		this.author = author;
+		this.member = member;
 	}
 
-	public Author getAuthor() {
-		return author;
+	public Author getMember() {
+		return member;
 	}
 
 }
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
index 76f46314e96b752f3aba60f3eb69d3f1ee0d3281..26e7ae9c7b73f533b5136fc05a709d28347e171d 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
@@ -1,20 +1,58 @@
 package org.briarproject.api.privategroup;
 
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.PrivateKey;
+import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
-
-import java.security.GeneralSecurityException;
+import org.jetbrains.annotations.Nullable;
 
 public interface GroupMessageFactory {
 
-	@NotNull
+	/**
+	 * Creates a new member announcement that contains the joiner's identity
+	 * and is signed by the creator.
+	 * <p>
+	 * When a new member accepts an invitation to the group,
+	 * the creator sends this new member announcement to the group.
+	 *
+	 * @param groupId   The ID of the group the new member joined
+	 * @param timestamp The current timestamp
+	 * @param creator   The creator of the group with {@param groupId}
+	 * @param member    The new member that has just accepted an invitation
+	 */
+	@CryptoExecutor
+	GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
+			LocalAuthor creator, Author member);
+
+	/**
+	 * Creates a join announcement message
+	 * that depends on a previous new member announcement.
+	 *
+	 * @param groupId     The ID of the Group that is being joined
+	 * @param timestamp   Must be equal to the timestamp of the new member message
+	 * @param member      Our own LocalAuthor
+	 * @param newMemberId The MessageId of the new member message
+	 */
+	@CryptoExecutor
+	GroupMessage createJoinMessage(GroupId groupId, long timestamp,
+			LocalAuthor member, MessageId newMemberId);
+
+	/**
+	 * Creates a group message
+	 *
+	 * @param groupId       The ID of the Group that is posted in
+	 * @param timestamp     Must be greater than the timestamps of the parentId
+	 *                      post, if any, and the member's previous message
+	 * @param parentId      The ID of the message that is replied to
+	 * @param author        The author of the group message
+	 * @param body          The content of the group message
+	 * @param previousMsgId The ID of the author's previous message
+	 *                      in this group
+	 */
+	@CryptoExecutor
 	GroupMessage createGroupMessage(GroupId groupId, long timestamp,
-			MessageId parent, LocalAuthor author, String body)
-			throws FormatException, GeneralSecurityException;
+			@Nullable MessageId parentId, LocalAuthor author, String body,
+			MessageId previousMsgId);
 
 }
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
index 21043297bb338e51de4b76a73800dd970207280a..9789a59e0ac2e31ddd1e46a7ad7bb69c1fe78bbe 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
@@ -3,19 +3,23 @@ package org.briarproject.api.privategroup;
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
 public class GroupMessageHeader extends PostHeader {
 
 	private final GroupId groupId;
 
-	public GroupMessageHeader(@NotNull GroupId groupId, @NotNull MessageId id,
+	public GroupMessageHeader(GroupId groupId, MessageId id,
 			@Nullable MessageId parentId, long timestamp,
-			@NotNull Author author, @NotNull Status authorStatus,
-			boolean read) {
+			Author author, Status authorStatus, boolean read) {
 		super(id, parentId, timestamp, author, authorStatus, read);
 		this.groupId = groupId;
 	}
diff --git a/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java b/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef8c3b337d72124fd5d42b075f5e8edea894a53f
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/JoinMessageHeader.java
@@ -0,0 +1,21 @@
+package org.briarproject.api.privategroup;
+
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class JoinMessageHeader extends GroupMessageHeader {
+
+	public JoinMessageHeader(GroupId groupId, MessageId id,
+			@Nullable MessageId parentId, long timestamp, Author author,
+			Author.Status authorStatus, boolean read) {
+		super(groupId, id, parentId, timestamp, author, authorStatus, read);
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/MessageType.java b/briar-api/src/org/briarproject/api/privategroup/MessageType.java
new file mode 100644
index 0000000000000000000000000000000000000000..7cffb0df312973ac3b4955547037782aa7a294fe
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/privategroup/MessageType.java
@@ -0,0 +1,22 @@
+package org.briarproject.api.privategroup;
+
+public enum MessageType {
+	NEW_MEMBER(0),
+	JOIN(1),
+	POST(2);
+
+	int value;
+
+	MessageType(int value) {
+		this.value = value;
+	}
+
+	public static MessageType valueOf(int value) {
+		for (MessageType m : values()) if (m.value == value) return m;
+		throw new IllegalArgumentException();
+	}
+
+	public int getInt() {
+		return value;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
index f2253c99a7c0c1999c1910146602e586ba4cf153..49bee81f8788f1edacda917d4d0244544fe001e8 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
@@ -3,57 +3,59 @@ package org.briarproject.api.privategroup;
 import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
-import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 import java.util.Collection;
 
 public interface PrivateGroupManager extends MessageTracker {
 
 	/** Returns the unique ID of the private group client. */
-	@NotNull
 	ClientId getClientId();
 
-	/** Adds a new private group. */
-	GroupId addPrivateGroup(String name) throws DbException;
+	/**
+	 * Adds a new private group and joins it.
+	 *
+	 * @param group        The private group to add
+	 * @param newMemberMsg The creator's message announcing herself as
+	 *                     first new member
+	 * @param joinMsg      The creator's own join message
+	 */
+	void addPrivateGroup(PrivateGroup group, GroupMessage newMemberMsg,
+			GroupMessage joinMsg) throws DbException;
 
 	/** Removes a dissolved private group. */
 	void removePrivateGroup(GroupId g) throws DbException;
 
-	/** Creates a local group message. */
-	GroupMessage createLocalMessage(GroupId groupId, String body,
-			long timestamp, @Nullable MessageId parentId, LocalAuthor author);
+	/** Gets the MessageId of your previous message sent to the group */
+	MessageId getPreviousMsgId(GroupId g) throws DbException;
+
+	/** Returns the timestamp of the message with the given ID */
+	// TODO change to getPreviousMessageHeader()
+	long getMessageTimestamp(MessageId id) throws DbException;
 
 	/** Stores (and sends) a local group message. */
 	GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
 
 	/** Returns the private group with the given ID. */
-	@NotNull
 	PrivateGroup getPrivateGroup(GroupId g) throws DbException;
 
 	/**
 	 * Returns the private group with the given ID within the given transaction.
 	 */
-	@NotNull
 	PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
 
 	/** Returns all private groups the user is a member of. */
-	@NotNull
 	Collection<PrivateGroup> getPrivateGroups() throws DbException;
 
 	/** Returns true if the private group has been dissolved. */
 	boolean isDissolved(GroupId g) throws DbException;
 
 	/** Returns the body of the group message with the given ID. */
-	@NotNull
 	String getMessageBody(MessageId m) throws DbException;
 
 	/** Returns the headers of all group messages in the given group. */
-	@NotNull
 	Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
 
 }
diff --git a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
index 29908c6d3d8230368e9fe4abc508e2eeefe12f8e..cd2347bc65ea79d8b3110ff28b556fab4a169eb4 100644
--- a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
+++ b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
@@ -6,10 +6,6 @@ import org.briarproject.api.blogs.BlogFactory;
 import org.briarproject.api.blogs.MessageType;
 import org.briarproject.api.clients.BdfMessageContext;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PublicKey;
-import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
@@ -48,18 +44,15 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
 
 class BlogPostValidator extends BdfMessageValidator {
 
-	private final CryptoComponent crypto;
 	private final GroupFactory groupFactory;
 	private final MessageFactory messageFactory;
 	private final BlogFactory blogFactory;
 
-	BlogPostValidator(CryptoComponent crypto, GroupFactory groupFactory,
-			MessageFactory messageFactory, BlogFactory blogFactory,
-			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
-			Clock clock) {
+	BlogPostValidator(GroupFactory groupFactory, MessageFactory messageFactory,
+			BlogFactory blogFactory, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
 		super(clientHelper, metadataEncoder, clock);
 
-		this.crypto = crypto;
 		this.groupFactory = groupFactory;
 		this.messageFactory = messageFactory;
 		this.blogFactory = blogFactory;
@@ -109,7 +102,11 @@ class BlogPostValidator extends BdfMessageValidator {
 		BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), postBody);
 		Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
 		Author a = b.getAuthor();
-		verifySignature(sig, a.getPublicKey(), signed);
+		try {
+			clientHelper.verifySignature(sig, a.getPublicKey(), signed);
+		} catch (GeneralSecurityException e) {
+			throw new InvalidMessageException(e);
+		}
 
 		// Return the metadata and dependencies
 		BdfDictionary meta = new BdfDictionary();
@@ -150,7 +147,11 @@ class BlogPostValidator extends BdfMessageValidator {
 						currentId);
 		Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
 		Author a = b.getAuthor();
-		verifySignature(sig, a.getPublicKey(), signed);
+		try {
+			clientHelper.verifySignature(sig, a.getPublicKey(), signed);
+		} catch (GeneralSecurityException e) {
+			throw new InvalidMessageException(e);
+		}
 
 		// Return the metadata and dependencies
 		BdfDictionary meta = new BdfDictionary();
@@ -267,26 +268,6 @@ class BlogPostValidator extends BdfMessageValidator {
 		return new BdfMessageContext(meta, dependencies);
 	}
 
-	private void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
-			throws InvalidMessageException {
-		try {
-			// Parse the public key
-			KeyParser keyParser = crypto.getSignatureKeyParser();
-			PublicKey key = keyParser.parsePublicKey(publicKey);
-			// Verify the signature
-			Signature signature = crypto.getSignature();
-			signature.initVerify(key);
-			signature.update(clientHelper.toByteArray(signed));
-			if (!signature.verify(sig)) {
-				throw new InvalidMessageException("Invalid signature");
-			}
-		} catch (GeneralSecurityException e) {
-			throw new InvalidMessageException("Invalid public key");
-		} catch (FormatException e) {
-			throw new InvalidMessageException(e);
-		}
-	}
-
 	static BdfDictionary authorToBdfDictionary(Author a) {
 		return BdfDictionary.of(
 				new BdfEntry(KEY_AUTHOR_ID, a.getId()),
diff --git a/briar-core/src/org/briarproject/blogs/BlogsModule.java b/briar-core/src/org/briarproject/blogs/BlogsModule.java
index 91cc68c7c5ff2d5052367aff744d16ace49a2293..e8927d5afc374a7c4b29d8593b9214810f922150 100644
--- a/briar-core/src/org/briarproject/blogs/BlogsModule.java
+++ b/briar-core/src/org/briarproject/blogs/BlogsModule.java
@@ -5,7 +5,6 @@ import org.briarproject.api.blogs.BlogManager;
 import org.briarproject.api.blogs.BlogPostFactory;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
@@ -64,14 +63,14 @@ public class BlogsModule {
 	@Provides
 	@Singleton
 	BlogPostValidator provideBlogPostValidator(
-			ValidationManager validationManager, CryptoComponent crypto,
-			GroupFactory groupFactory, MessageFactory messageFactory,
-			BlogFactory blogFactory, ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
+			ValidationManager validationManager, GroupFactory groupFactory,
+			MessageFactory messageFactory, BlogFactory blogFactory,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
 
-		BlogPostValidator validator = new BlogPostValidator(crypto,
-				groupFactory, messageFactory, blogFactory, clientHelper,
-				metadataEncoder, clock);
+		BlogPostValidator validator = new BlogPostValidator(groupFactory,
+				messageFactory, blogFactory, clientHelper, metadataEncoder,
+				clock);
 		validationManager.registerMessageValidator(CLIENT_ID, validator);
 
 		return validator;
diff --git a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
index d7a983b0ae43b2fcc9e01832b1ed47efb4572148..9c0354419a0250f0ae85c122e7da4519d99c1f48 100644
--- a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
+++ b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
@@ -5,6 +5,7 @@ import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyParser;
 import org.briarproject.api.crypto.PrivateKey;
+import org.briarproject.api.crypto.PublicKey;
 import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
@@ -320,4 +321,20 @@ class ClientHelperImpl implements ClientHelper {
 		signature.update(toByteArray(toSign));
 		return signature.sign();
 	}
+
+	@Override
+	public void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
+			throws FormatException, GeneralSecurityException {
+		// Parse the public key
+		KeyParser keyParser = cryptoComponent.getSignatureKeyParser();
+		PublicKey key = keyParser.parsePublicKey(publicKey);
+		// Verify the signature
+		Signature signature = cryptoComponent.getSignature();
+		signature.initVerify(key);
+		signature.update(toByteArray(signed));
+		if (!signature.verify(sig)) {
+			throw new GeneralSecurityException("Invalid signature");
+		}
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index 40bfd01381fe8b1972fb253ec389c7b18107e006..eeafb54dfc46b9ab7b12cff3ea93fef5e7944b1e 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -54,11 +54,11 @@ public class ForumModule {
 	@Provides
 	@Singleton
 	ForumPostValidator provideForumPostValidator(
-			ValidationManager validationManager, CryptoComponent crypto,
-			AuthorFactory authorFactory, ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-		ForumPostValidator validator = new ForumPostValidator(crypto,
-				authorFactory, clientHelper, metadataEncoder, clock);
+			ValidationManager validationManager, AuthorFactory authorFactory,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
+		ForumPostValidator validator = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
 		validationManager.registerMessageValidator(
 				ForumManagerImpl.CLIENT_ID, validator);
 		return validator;
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index 258ac9bb5f23e6305864d46d6076284b93f86e35..ce4bddb4364212326571fdce95d9dee020749dfc 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -4,10 +4,6 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
 import org.briarproject.api.clients.BdfMessageContext;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PublicKey;
-import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
@@ -32,14 +28,11 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
 
 class ForumPostValidator extends BdfMessageValidator {
 
-	private final CryptoComponent crypto;
 	private final AuthorFactory authorFactory;
 
-	ForumPostValidator(CryptoComponent crypto, AuthorFactory authorFactory,
-			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
-			Clock clock) {
+	ForumPostValidator(AuthorFactory authorFactory, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
 		super(clientHelper, metadataEncoder, clock);
-		this.crypto = crypto;
 		this.authorFactory = authorFactory;
 	}
 
@@ -81,22 +74,14 @@ class ForumPostValidator extends BdfMessageValidator {
 		}
 		// Verify the signature, if any
 		if (author != null) {
+			// Serialise the data to be verified
+			BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
+					authorList, contentType, forumPostBody);
 			try {
-				// Parse the public key
-				KeyParser keyParser = crypto.getSignatureKeyParser();
-				PublicKey key = keyParser.parsePublicKey(author.getPublicKey());
-				// Serialise the data to be signed
-				BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
-						authorList, contentType, forumPostBody);
-				// Verify the signature
-				Signature signature = crypto.getSignature();
-				signature.initVerify(key);
-				signature.update(clientHelper.toByteArray(signed));
-				if (!signature.verify(sig)) {
-					throw new InvalidMessageException("Invalid signature");
-				}
+				clientHelper
+						.verifySignature(sig, author.getPublicKey(), signed);
 			} catch (GeneralSecurityException e) {
-				throw new InvalidMessageException("Invalid public key");
+				throw new InvalidMessageException(e);
 			}
 		}
 		// Return the metadata and dependencies
diff --git a/briar-core/src/org/briarproject/privategroup/Constants.java b/briar-core/src/org/briarproject/privategroup/Constants.java
index 7a1a4495bf104b7c083cf96bbb5965f20024c7c1..12219f78d789193398edea716259c41cba7f7847 100644
--- a/briar-core/src/org/briarproject/privategroup/Constants.java
+++ b/briar-core/src/org/briarproject/privategroup/Constants.java
@@ -1,8 +1,18 @@
 package org.briarproject.privategroup;
 
+import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
+
 interface Constants {
 
 	// Database keys
-	String KEY_READ = "read";
+	String KEY_TYPE = "type";
+	String KEY_TIMESTAMP = "timestamp";
+	String KEY_READ = MSG_KEY_READ;
+	String KEY_PARENT_MSG_ID = "parentMsgId";
+	String KEY_NEW_MEMBER_MSG_ID = "newMemberMsgId";
+	String KEY_PREVIOUS_MSG_ID = "previousMsgId";
+	String KEY_MEMBER_ID = "memberId";
+	String KEY_MEMBER_NAME = "memberName";
+	String KEY_MEMBER_PUBLIC_KEY = "memberPublicKey";
 
 }
diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
index 271f7712c02c3b24373bddbb715a77e36f490400..228b6425448958ae1ebaee41e372160b8092a078 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
@@ -3,18 +3,25 @@ package org.briarproject.privategroup;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfList;
+import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.privategroup.GroupMessage;
 import org.briarproject.api.privategroup.GroupMessageFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
-import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.security.GeneralSecurityException;
 
 import javax.inject.Inject;
 
+import static org.briarproject.api.privategroup.MessageType.JOIN;
+import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
+import static org.briarproject.api.privategroup.MessageType.POST;
+
+@NotNullByDefault
 class GroupMessageFactoryImpl implements GroupMessageFactory {
 
 	private final ClientHelper clientHelper;
@@ -24,20 +31,82 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
 		this.clientHelper = clientHelper;
 	}
 
-	@NotNull
 	@Override
-	public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
-			MessageId parent, LocalAuthor author, String body)
-			throws FormatException, GeneralSecurityException {
+	public GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
+			LocalAuthor creator, Author member) {
+		try {
+			// Generate the signature
+			int type = NEW_MEMBER.getInt();
+			BdfList toSign = BdfList.of(groupId, timestamp, type,
+					member.getName(), member.getPublicKey());
+			byte[] signature =
+					clientHelper.sign(toSign, creator.getPrivateKey());
+
+			// Compose the message
+			BdfList body =
+					BdfList.of(type, member.getName(),
+							member.getPublicKey(), signature);
+			Message m = clientHelper.createMessage(groupId, timestamp, body);
+
+			return new GroupMessage(m, null, member);
+		} catch (GeneralSecurityException e) {
+			throw new RuntimeException(e);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
+			LocalAuthor member, MessageId newMemberId) {
+		try {
+			// Generate the signature
+			int type = JOIN.getInt();
+			BdfList toSign = BdfList.of(groupId, timestamp, type,
+					member.getName(), member.getPublicKey(), newMemberId);
+			byte[] signature =
+					clientHelper.sign(toSign, member.getPrivateKey());
+
+			// Compose the message
+			BdfList body =
+					BdfList.of(type, member.getName(),
+							member.getPublicKey(), newMemberId, signature);
+			Message m = clientHelper.createMessage(groupId, timestamp, body);
+
+			return new GroupMessage(m, null, member);
+		} catch (GeneralSecurityException e) {
+			throw new RuntimeException(e);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
+	}
 
-		// Generate the signature
-		byte[] sig = clientHelper.sign(new BdfList(), author.getPrivateKey());
+	@Override
+	public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
+			@Nullable MessageId parentId, LocalAuthor author, String content,
+			MessageId previousMsgId) {
+		try {
+			// Generate the signature
+			int type = POST.getInt();
+			BdfList toSign = BdfList.of(groupId, timestamp, type,
+					author.getName(), author.getPublicKey(), parentId,
+					previousMsgId, content);
+			byte[] signature =
+					clientHelper.sign(toSign, author.getPrivateKey());
 
-		// Compose the message
-		Message m =
-				clientHelper.createMessage(groupId, timestamp, new BdfList());
+			// Compose the message
+			BdfList body =
+					BdfList.of(type, author.getName(),
+							author.getPublicKey(), parentId, previousMsgId,
+							content, signature);
+			Message m = clientHelper.createMessage(groupId, timestamp, body);
 
-		return new GroupMessage(m, parent, author);
+			return new GroupMessage(m, parentId, author);
+		} catch (GeneralSecurityException e) {
+			throw new RuntimeException(e);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
 	}
 
 }
diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
index 75a8913c07a677efdeb4d5ef01cb85487bb473bf..25bf14730cc4b988b91728de14f9ef10fa22f2f7 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
@@ -3,11 +3,14 @@ package org.briarproject.privategroup;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.BdfMessageContext;
 import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.privategroup.MessageType;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
@@ -15,19 +18,38 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.system.Clock;
 import org.briarproject.clients.BdfMessageValidator;
 
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 
+import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+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.privategroup.MessageType.JOIN;
+import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
+import static org.briarproject.api.privategroup.MessageType.POST;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
+import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_READ;
+import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
+import static org.briarproject.privategroup.Constants.KEY_TYPE;
+
 class GroupMessageValidator extends BdfMessageValidator {
 
-	private final CryptoComponent crypto;
+	private final PrivateGroupFactory groupFactory;
 	private final AuthorFactory authorFactory;
 
-	GroupMessageValidator(CryptoComponent crypto, AuthorFactory authorFactory,
+	GroupMessageValidator(PrivateGroupFactory groupFactory,
 			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
-			Clock clock) {
+			Clock clock, AuthorFactory authorFactory) {
 		super(clientHelper, metadataEncoder, clock);
-		this.crypto = crypto;
+		this.groupFactory = groupFactory;
 		this.authorFactory = authorFactory;
 	}
 
@@ -35,9 +57,168 @@ class GroupMessageValidator extends BdfMessageValidator {
 	protected BdfMessageContext validateMessage(Message m, Group g,
 			BdfList body) throws InvalidMessageException, FormatException {
 
+		checkSize(body, 4, 7);
+
+		// message type (int)
+		int type = body.getLong(0).intValue();
+		body.removeElementAt(0);
+
+		// member_name (string)
+		String memberName = body.getString(0);
+		checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH);
+
+		// member_public_key (raw)
+		byte[] memberPublicKey = body.getRaw(1);
+		checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
+
+		BdfMessageContext c;
+		switch (MessageType.valueOf(type)) {
+			case NEW_MEMBER:
+				c = validateNewMember(m, g, body, memberName,
+						memberPublicKey);
+				addMessageMetadata(c, memberName, memberPublicKey,
+						m.getTimestamp());
+				break;
+			case JOIN:
+				c = validateJoin(m, g, body, memberName, memberPublicKey);
+				addMessageMetadata(c, memberName, memberPublicKey,
+						m.getTimestamp());
+				break;
+			case POST:
+				c = validatePost(m, g, body, memberName, memberPublicKey);
+				addMessageMetadata(c, memberName, memberPublicKey,
+						m.getTimestamp());
+				break;
+			default:
+				throw new InvalidMessageException("Unknown Message Type");
+		}
+		c.getDictionary().put(KEY_TYPE, type);
+		return c;
+	}
+
+	private BdfMessageContext validateNewMember(Message m, Group g,
+			BdfList body, String memberName, byte[] memberPublicKey)
+			throws InvalidMessageException, FormatException {
+
+		// The content is a BDF list with three elements
+		checkSize(body, 3);
+
+		// signature (raw)
+		// signature with the creator's private key over a list with 4 elements
+		byte[] signature = body.getRaw(2);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		// Verify Signature
+		BdfList signed =
+				BdfList.of(g.getId(), m.getTimestamp(), NEW_MEMBER.getInt(),
+						memberName, memberPublicKey);
+		PrivateGroup group = groupFactory.parsePrivateGroup(g);
+		byte[] creatorPublicKey = group.getAuthor().getPublicKey();
+		try {
+			clientHelper.verifySignature(signature, creatorPublicKey, signed);
+		} catch (GeneralSecurityException e) {
+			throw new InvalidMessageException(e);
+		}
+
+		// Return the metadata and no dependencies
+		BdfDictionary meta = new BdfDictionary();
+		return new BdfMessageContext(meta);
+	}
+
+	private BdfMessageContext validateJoin(Message m, Group g, BdfList body,
+			String memberName, byte[] memberPublicKey)
+			throws InvalidMessageException, FormatException {
+
+		// The content is a BDF list with four elements
+		checkSize(body, 4);
+
+		// new_member_id (raw)
+		// the identifier of a new member message
+		// with the same member_name and member_public_key
+		byte[] newMemberId = body.getRaw(2);
+		checkLength(newMemberId, MessageId.LENGTH);
+
+		// signature (raw)
+		// a signature with the member's private key over a list with 5 elements
+		byte[] signature = body.getRaw(3);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		// Verify Signature
+		BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
+				memberName, memberPublicKey, newMemberId);
+		try {
+			clientHelper.verifySignature(signature, memberPublicKey, signed);
+		} catch (GeneralSecurityException e) {
+			throw new InvalidMessageException(e);
+		}
+
+		// The new member message is a dependency
+		Collection<MessageId> dependencies =
+				Collections.singleton(new MessageId(newMemberId));
+
+		// Return the metadata and dependencies
 		BdfDictionary meta = new BdfDictionary();
-		Collection<MessageId> dependencies = Collections.emptyList();
+		meta.put(KEY_NEW_MEMBER_MSG_ID, newMemberId);
 		return new BdfMessageContext(meta, dependencies);
 	}
 
+	private BdfMessageContext validatePost(Message m, Group g, BdfList body,
+			String memberName, byte[] memberPublicKey)
+			throws InvalidMessageException, FormatException {
+
+		// The content is a BDF list with six elements
+		checkSize(body, 6);
+
+		// parent_id (raw or null)
+		// the identifier of the post to which this is a reply, if any
+		byte[] parentId = body.getOptionalRaw(2);
+		checkLength(parentId, MessageId.LENGTH);
+
+		// previous_message_id (raw)
+		// the identifier of the member's previous post or join message
+		byte[] previousMessageId = body.getRaw(3);
+		checkLength(previousMessageId, MessageId.LENGTH);
+
+		// content (string)
+		String content = body.getString(4);
+		checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH);
+
+		// signature (raw)
+		// a signature with the member's private key over a list with 7 elements
+		byte[] signature = body.getRaw(5);
+		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
+
+		// Verify Signature
+		BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(),
+				memberName, memberPublicKey, parentId, previousMessageId,
+				content);
+		try {
+			clientHelper.verifySignature(signature, memberPublicKey, signed);
+		} catch (GeneralSecurityException e) {
+			throw new InvalidMessageException(e);
+		}
+
+		// The parent post, if any,
+		// and the member's previous message are dependencies
+		Collection<MessageId> dependencies = new ArrayList<MessageId>();
+		if (parentId != null) dependencies.add(new MessageId(parentId));
+		dependencies.add(new MessageId(previousMessageId));
+
+		// Return the metadata and dependencies
+		BdfDictionary meta = new BdfDictionary();
+		if (parentId != null) meta.put(KEY_PARENT_MSG_ID, parentId);
+		meta.put(KEY_PREVIOUS_MSG_ID, previousMessageId);
+		return new BdfMessageContext(meta, dependencies);
+	}
+
+	private void addMessageMetadata(BdfMessageContext c, String authorName,
+			byte[] pubKey, long time) {
+		c.getDictionary().put(KEY_TIMESTAMP, time);
+		c.getDictionary().put(KEY_READ, false);
+		Author a = authorFactory.createAuthor(authorName, pubKey);
+		c.getDictionary().put(KEY_MEMBER_ID, a.getId());
+		c.getDictionary().put(KEY_MEMBER_NAME, authorName);
+		c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, pubKey);
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index 05c2d4021c4c766a2cdbcb89baea7e792a594aeb..769e065b87a200cd8e80b2cbcc64bb4c42fe5437 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -3,16 +3,20 @@ package org.briarproject.privategroup;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager;
-import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.privategroup.GroupMessage;
-import org.briarproject.api.privategroup.GroupMessageFactory;
 import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.JoinMessageHeader;
+import org.briarproject.api.privategroup.MessageType;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.PrivateGroupManager;
@@ -21,21 +25,34 @@ import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
-import org.briarproject.api.system.Clock;
 import org.briarproject.clients.BdfIncomingMessageHook;
 import org.briarproject.util.StringUtils;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
-import java.security.GeneralSecurityException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
 import static org.briarproject.api.identity.Author.Status.OURSELVES;
+import static org.briarproject.api.privategroup.MessageType.JOIN;
+import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
+import static org.briarproject.api.privategroup.MessageType.POST;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
+import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
+import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
+import static org.briarproject.privategroup.Constants.KEY_READ;
+import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
+import static org.briarproject.privategroup.Constants.KEY_TYPE;
 
 public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		PrivateGroupManager {
@@ -46,62 +63,102 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 			StringUtils.fromHexString("5072697661746547726f75704d616e61"
 					+ "67657220627920546f727374656e2047"));
 
-	private final IdentityManager identityManager;
 	private final PrivateGroupFactory privateGroupFactory;
-	private final GroupMessageFactory groupMessageFactory;
-	private final Clock clock;
+	private final IdentityManager identityManager;
 
 	@Inject
 	PrivateGroupManagerImpl(ClientHelper clientHelper,
 			MetadataParser metadataParser, DatabaseComponent db,
-			IdentityManager identityManager,
 			PrivateGroupFactory privateGroupFactory,
-			GroupMessageFactory groupMessageFactory, Clock clock) {
+			IdentityManager identityManager) {
 		super(db, clientHelper, metadataParser);
 
-		this.identityManager = identityManager;
 		this.privateGroupFactory = privateGroupFactory;
-		this.groupMessageFactory = groupMessageFactory;
-		this.clock = clock;
+		this.identityManager = identityManager;
 	}
 
-	@NotNull
 	@Override
 	public ClientId getClientId() {
 		return CLIENT_ID;
 	}
 
 	@Override
-	public GroupId addPrivateGroup(String name) throws DbException {
-		PrivateGroup group;
+	public void addPrivateGroup(PrivateGroup group,
+			GroupMessage newMemberMsg, GroupMessage joinMsg)
+			throws DbException {
 		Transaction txn = db.startTransaction(false);
 		try {
-			LocalAuthor a = identityManager.getLocalAuthor(txn);
-			group = privateGroupFactory.createPrivateGroup(name, a);
 			db.addGroup(txn, group.getGroup());
+			announceNewMember(txn, newMemberMsg);
+			joinPrivateGroup(txn, joinMsg);
 			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
-		return group.getId();
+	}
+
+	private void announceNewMember(Transaction txn, GroupMessage m)
+			throws DbException, FormatException {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(KEY_TYPE, NEW_MEMBER.getInt());
+		addMessageMetadata(meta, m, true);
+		clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
+	}
+
+	private void joinPrivateGroup(Transaction txn, GroupMessage m)
+			throws DbException, FormatException {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(KEY_TYPE, JOIN.getInt());
+		addMessageMetadata(meta, m, true);
+		clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
+		trackOutgoingMessage(txn, m.getMessage());
+		setPreviousMsgId(txn, m.getMessage().getGroupId(),
+				m.getMessage().getId());
 	}
 
 	@Override
 	public void removePrivateGroup(GroupId g) throws DbException {
+		// TODO
+	}
 
+	@Override
+	public MessageId getPreviousMsgId(GroupId g) throws DbException {
+		MessageId previousMsgId;
+		Transaction txn = db.startTransaction(true);
+		try {
+			previousMsgId = getPreviousMsgId(txn, g);
+			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return previousMsgId;
+	}
+
+	private MessageId getPreviousMsgId(Transaction txn, GroupId g)
+			throws DbException, FormatException {
+		BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
+		byte[] previousMsgIdBytes = d.getRaw(KEY_PREVIOUS_MSG_ID);
+		return new MessageId(previousMsgIdBytes);
+	}
+
+	private void setPreviousMsgId(Transaction txn, GroupId g,
+			MessageId previousMsgId) throws DbException, FormatException {
+		BdfDictionary d = BdfDictionary
+				.of(new BdfEntry(KEY_PREVIOUS_MSG_ID, previousMsgId));
+		clientHelper.mergeGroupMetadata(txn, g, d);
 	}
 
 	@Override
-	public GroupMessage createLocalMessage(GroupId groupId, String body,
-			long timestamp, @Nullable MessageId parentId, LocalAuthor author) {
+	public long getMessageTimestamp(MessageId id) throws DbException {
 		try {
-			return groupMessageFactory
-					.createGroupMessage(groupId, timestamp, parentId, author,
-							body);
+			BdfDictionary d = clientHelper.getMessageMetadataAsDictionary(id);
+			return d.getLong(KEY_TIMESTAMP);
 		} catch (FormatException e) {
-			throw new RuntimeException(e);
-		} catch (GeneralSecurityException e) {
-			throw new RuntimeException(e);
+			throw new DbException(e);
 		}
 	}
 
@@ -111,7 +168,12 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		Transaction txn = db.startTransaction(false);
 		try {
 			BdfDictionary meta = new BdfDictionary();
+			meta.put(KEY_TYPE, POST.getInt());
+			if (m.getParent() != null) meta.put(KEY_PARENT_MSG_ID, m.getParent());
+			addMessageMetadata(meta, m, true);
 			clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
+			setPreviousMsgId(txn, m.getMessage().getGroupId(),
+					m.getMessage().getId());
 			trackOutgoingMessage(txn, m.getMessage());
 			txn.setComplete();
 		} catch (FormatException e) {
@@ -121,10 +183,18 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		}
 		return new GroupMessageHeader(m.getMessage().getGroupId(),
 				m.getMessage().getId(), m.getParent(),
-				m.getMessage().getTimestamp(), m.getAuthor(), OURSELVES, true);
+				m.getMessage().getTimestamp(), m.getMember(), OURSELVES, true);
+	}
+
+	private void addMessageMetadata(BdfDictionary meta, GroupMessage m,
+			boolean read) {
+		meta.put(KEY_TIMESTAMP, m.getMessage().getTimestamp());
+		meta.put(KEY_READ, read);
+		meta.put(KEY_MEMBER_ID, m.getMember().getId());
+		meta.put(KEY_MEMBER_NAME, m.getMember().getName());
+		meta.put(KEY_MEMBER_PUBLIC_KEY, m.getMember().getPublicKey());
 	}
 
-	@NotNull
 	@Override
 	public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
 		PrivateGroup privateGroup;
@@ -138,7 +208,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		return privateGroup;
 	}
 
-	@NotNull
 	@Override
 	public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
 			throws DbException {
@@ -150,7 +219,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		}
 	}
 
-	@NotNull
 	@Override
 	public Collection<PrivateGroup> getPrivateGroups() throws DbException {
 		Collection<Group> groups;
@@ -178,27 +246,179 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		return false;
 	}
 
-	@NotNull
 	@Override
 	public String getMessageBody(MessageId m) throws DbException {
-		return "empty";
+		try {
+			// type(0), member_name(1), member_public_key(2), parent_id(3),
+			// previous_message_id(4), content(5), signature(6)
+			return clientHelper.getMessageAsList(m).getString(5);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
 	}
 
-	@NotNull
 	@Override
 	public Collection<GroupMessageHeader> getHeaders(GroupId g)
 			throws DbException {
+		Collection<GroupMessageHeader> headers =
+				new ArrayList<GroupMessageHeader>();
+		Transaction txn = db.startTransaction(true);
+		try {
+			Map<MessageId, BdfDictionary> metadata =
+					clientHelper.getMessageMetadataAsDictionary(txn, g);
+			// get all authors we need to get the status for
+			Set<AuthorId> authors = new HashSet<AuthorId>();
+			for (BdfDictionary meta : metadata.values()) {
+				if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
+					continue;
+				byte[] idBytes = meta.getRaw(KEY_MEMBER_ID);
+				authors.add(new AuthorId(idBytes));
+			}
+			// get statuses for all authors
+			Map<AuthorId, Status> statuses = new HashMap<AuthorId, Status>();
+			for (AuthorId id : authors) {
+				statuses.put(id, identityManager.getAuthorStatus(txn, id));
+			}
+			// Parse the metadata
+			for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
+				BdfDictionary meta = entry.getValue();
+				if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
+					continue;
+				headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta,
+						statuses));
+			}
+			txn.setComplete();
+			return headers;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private GroupMessageHeader getGroupMessageHeader(Transaction txn, GroupId g,
+			MessageId id, BdfDictionary meta, Map<AuthorId, Status> statuses)
+			throws DbException, FormatException {
+
+		MessageId parentId = null;
+		if (meta.containsKey(KEY_PARENT_MSG_ID)) {
+			parentId = new MessageId(meta.getRaw(KEY_PARENT_MSG_ID));
+		}
+		long timestamp = meta.getLong(KEY_TIMESTAMP);
 
-		return Collections.emptyList();
+		AuthorId authorId = new AuthorId(meta.getRaw(KEY_MEMBER_ID));
+		String name = meta.getString(KEY_MEMBER_NAME);
+		byte[] publicKey = meta.getRaw(KEY_MEMBER_PUBLIC_KEY);
+		Author author = new Author(authorId, name, publicKey);
+
+		Status status;
+		if (statuses.containsKey(authorId)) {
+			status = statuses.get(authorId);
+		} else {
+			status = identityManager.getAuthorStatus(txn, author.getId());
+		}
+		boolean read = meta.getBoolean(KEY_READ);
+
+		if (meta.getLong(KEY_TYPE) == JOIN.getInt()) {
+			return new JoinMessageHeader(g, id, parentId, timestamp, author,
+					status, read);
+		}
+		return new GroupMessageHeader(g, id, parentId, timestamp, author,
+				status, read);
 	}
 
 	@Override
 	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
 			BdfDictionary meta) throws DbException, FormatException {
 
-		trackIncomingMessage(txn, m);
-
-		return true;
+		long timestamp = meta.getLong(KEY_TIMESTAMP);
+		MessageType type =
+				MessageType.valueOf(meta.getLong(KEY_TYPE).intValue());
+		switch (type) {
+			case NEW_MEMBER:
+				// don't track incoming message, because it won't show in the UI
+				return true;
+			case JOIN:
+				// new_member_id must be the identifier of a NEW_MEMBER message
+				byte[] newMemberIdBytes =
+						meta.getOptionalRaw(KEY_NEW_MEMBER_MSG_ID);
+				MessageId newMemberId = new MessageId(newMemberIdBytes);
+				BdfDictionary newMemberMeta = clientHelper
+						.getMessageMetadataAsDictionary(txn, newMemberId);
+				MessageType newMemberType = MessageType
+						.valueOf(newMemberMeta.getLong(KEY_TYPE).intValue());
+				if (newMemberType != NEW_MEMBER) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				// timestamp must be equal to timestamp of NEW_MEMBER message
+				if (timestamp != newMemberMeta.getLong(KEY_TIMESTAMP)) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				// NEW_MEMBER must have same member_name and member_public_key
+				if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
+						newMemberMeta.getRaw(KEY_MEMBER_ID))) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				// TODO add to member list
+				trackIncomingMessage(txn, m);
+				return true;
+			case POST:
+				// timestamp must be greater than the timestamps of parent post
+				byte[] parentIdBytes = meta.getOptionalRaw(KEY_PARENT_MSG_ID);
+				if (parentIdBytes != null) {
+					MessageId parentId = new MessageId(parentIdBytes);
+					BdfDictionary parentMeta = clientHelper
+							.getMessageMetadataAsDictionary(txn, parentId);
+					if (timestamp <= parentMeta.getLong(KEY_TIMESTAMP)) {
+						// FIXME throw new InvalidMessageException() (#643)
+						db.deleteMessage(txn, m.getId());
+						return false;
+					}
+					MessageType parentType = MessageType
+							.valueOf(parentMeta.getLong(KEY_TYPE).intValue());
+					if (parentType != POST) {
+						// FIXME throw new InvalidMessageException() (#643)
+						db.deleteMessage(txn, m.getId());
+						return false;
+					}
+				}
+				// and the member's previous message
+				byte[] previousMsgIdBytes = meta.getRaw(KEY_PREVIOUS_MSG_ID);
+				MessageId previousMsgId = new MessageId(previousMsgIdBytes);
+				BdfDictionary previousMeta = clientHelper
+						.getMessageMetadataAsDictionary(txn, previousMsgId);
+				if (timestamp <= previousMeta.getLong(KEY_TIMESTAMP)) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				// previous message must be from same member
+				if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
+						previousMeta.getRaw(KEY_MEMBER_ID))) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				// previous message must be a POST or JOIN
+				MessageType previousType = MessageType
+						.valueOf(previousMeta.getLong(KEY_TYPE).intValue());
+				if (previousType != JOIN && previousType != POST) {
+					// FIXME throw new InvalidMessageException() (#643)
+					db.deleteMessage(txn, m.getId());
+					return false;
+				}
+				trackIncomingMessage(txn, m);
+				return true;
+			default:
+				// the validator should only let valid types pass
+				throw new RuntimeException("Unknown MessageType");
+		}
 	}
 
 }
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
index 570f697a629d952be8790dd5b27e04e1a21a2a90..49c0714c5cb0987c2fd5c602578ec498e42efdf0 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
@@ -2,7 +2,6 @@ package org.briarproject.privategroup;
 
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.lifecycle.LifecycleManager;
@@ -59,13 +58,17 @@ public class PrivateGroupModule {
 	@Provides
 	@Singleton
 	GroupMessageValidator provideGroupMessageValidator(
-			ValidationManager validationManager, CryptoComponent crypto,
-			AuthorFactory authorFactory, ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-		GroupMessageValidator validator = new GroupMessageValidator(crypto,
-				authorFactory, clientHelper, metadataEncoder, clock);
+			PrivateGroupFactory groupFactory,
+			ValidationManager validationManager, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock,
+			AuthorFactory authorFactory) {
+
+		GroupMessageValidator validator = new GroupMessageValidator(
+				groupFactory, clientHelper, metadataEncoder, clock,
+				authorFactory);
 		validationManager.registerMessageValidator(
 				PrivateGroupManagerImpl.CLIENT_ID, validator);
+
 		return validator;
 	}
 
diff --git a/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java
index bdbcc2398ce40f81da6e8b4fafc5509fb5832f37..afb1b90b9f5a78e5d3144d8acb37fdc6bd872319 100644
--- a/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java
+++ b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java
@@ -7,9 +7,6 @@ import org.briarproject.api.blogs.Blog;
 import org.briarproject.api.blogs.BlogFactory;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PublicKey;
-import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
@@ -20,7 +17,6 @@ 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.InvalidMessageException;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
@@ -37,9 +33,9 @@ import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR;
 import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID;
 import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME;
 import static org.briarproject.api.blogs.BlogConstants.KEY_COMMENT;
+import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
 import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_PARENT_MSG_ID;
 import static org.briarproject.api.blogs.BlogConstants.KEY_PARENT_MSG_ID;
-import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
 import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY;
 import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
 import static org.briarproject.api.blogs.MessageType.COMMENT;
@@ -94,9 +90,8 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		message = new Message(messageId, group.getId(), timestamp, raw);
 
 		MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
-		validator = new BlogPostValidator(cryptoComponent, groupFactory,
-				messageFactory, blogFactory, clientHelper, metadataEncoder,
-				clock);
+		validator = new BlogPostValidator(groupFactory, messageFactory,
+				blogFactory, clientHelper, metadataEncoder, clock);
 		context.assertIsSatisfied();
 	}
 
@@ -108,7 +103,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 
 		BdfList signed =
 				BdfList.of(blog.getId(), message.getTimestamp(), body);
-		expectCrypto(signed, sigBytes, true);
+		expectCrypto(signed, sigBytes);
 		final BdfDictionary result =
 				validator.validateMessage(message, group, m).getDictionary();
 
@@ -135,18 +130,6 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		validator.validateMessage(message, group, m).getDictionary();
 	}
 
-	@Test(expected = InvalidMessageException.class)
-	public void testValidateBlogPostWithBadSignature()
-			throws IOException, GeneralSecurityException {
-		final byte[] sigBytes = TestUtils.getRandomBytes(42);
-		BdfList m = BdfList.of(POST.getInt(), body, sigBytes);
-
-		BdfList signed =
-				BdfList.of(blog.getId(), message.getTimestamp(), body);
-		expectCrypto(signed, sigBytes, false);
-		validator.validateMessage(message, group, m).getDictionary();
-	}
-
 	@Test
 	public void testValidateProperBlogComment()
 			throws IOException, GeneralSecurityException {
@@ -162,7 +145,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		BdfList signed =
 				BdfList.of(blog.getId(), message.getTimestamp(), comment,
 						pOriginalId, currentId);
-		expectCrypto(signed, sigBytes, true);
+		expectCrypto(signed, sigBytes);
 		final BdfDictionary result =
 				validator.validateMessage(message, group, m).getDictionary();
 
@@ -189,7 +172,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		BdfList signed =
 				BdfList.of(blog.getId(), message.getTimestamp(), null,
 						originalId, currentId);
-		expectCrypto(signed, sigBytes, true);
+		expectCrypto(signed, sigBytes);
 		final BdfDictionary result =
 				validator.validateMessage(message, group, m).getDictionary();
 
@@ -208,7 +191,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 
 		BdfList signed =
 				BdfList.of(blog.getId(), message.getTimestamp(), body);
-		expectCrypto(signed, sigBytes, true);
+		expectCrypto(signed, sigBytes);
 
 		final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
 		final byte[] originalBody = TestUtils.getRandomBytes(42);
@@ -247,7 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
 
 		BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
 				comment, originalId, oldId);
-		expectCrypto(signed, sigBytes, true);
+		expectCrypto(signed, sigBytes);
 
 		final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
 				originalId, oldId, sigBytes);
@@ -275,27 +258,13 @@ public class BlogPostValidatorTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
-	private void expectCrypto(final BdfList signed, final byte[] sig,
-			final boolean pass) throws IOException, GeneralSecurityException {
-		final Signature signature = context.mock(Signature.class);
-		final KeyParser keyParser = context.mock(KeyParser.class);
-		final PublicKey publicKey = context.mock(PublicKey.class);
-
+	private void expectCrypto(final BdfList signed, final byte[] sig)
+			throws IOException, GeneralSecurityException {
 		context.checking(new Expectations() {{
 			oneOf(blogFactory).parseBlog(group, "");
 			will(returnValue(blog));
-			oneOf(cryptoComponent).getSignatureKeyParser();
-			will(returnValue(keyParser));
-			oneOf(keyParser).parsePublicKey(blog.getAuthor().getPublicKey());
-			will(returnValue(publicKey));
-			oneOf(cryptoComponent).getSignature();
-			will(returnValue(signature));
-			oneOf(signature).initVerify(publicKey);
-			oneOf(clientHelper).toByteArray(signed);
-			will(returnValue(sig));
-			oneOf(signature).update(sig);
-			oneOf(signature).verify(sig);
-			will(returnValue(pass));
+			oneOf(clientHelper)
+					.verifySignature(sig, author.getPublicKey(), signed);
 		}});
 	}