diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
index 75d8791084e0a8a70f72bef4ad2e5255e09a9693..786966df5a911b1c02bd833697169830e42214dd 100644
--- a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
@@ -2,12 +2,15 @@ package org.briarproject;
 
 import net.jodah.concurrentunit.Waiter;
 
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.ContactGroupFactory;
 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.data.BdfList;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.Event;
@@ -24,6 +27,8 @@ 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.privategroup.invitation.GroupInvitationManager;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.SyncSession;
@@ -53,6 +58,7 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import static org.briarproject.TestPluginsModule.MAX_LATENCY;
+import static org.briarproject.TestUtils.getRandomBytes;
 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;
@@ -72,18 +78,23 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 	private LocalAuthor author0, author1;
 	private PrivateGroup privateGroup0;
 	private GroupId groupId0;
-	private GroupMessage newMemberMsg0;
 
 	@Inject
 	Clock clock;
 	@Inject
 	AuthorFactory authorFactory;
 	@Inject
+	ClientHelper clientHelper;
+	@Inject
 	CryptoComponent crypto;
 	@Inject
+	ContactGroupFactory contactGroupFactory;
+	@Inject
 	PrivateGroupFactory privateGroupFactory;
 	@Inject
 	GroupMessageFactory groupMessageFactory;
+	@Inject
+	GroupInvitationManager groupInvitationManager;
 
 	// objects accessed from background threads need to be volatile
 	private volatile Waiter validationWaiter;
@@ -222,20 +233,6 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 
 		// 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
@@ -323,15 +320,13 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		addDefaultContacts();
 		listenToEvents();
 
-		// author0 joins privateGroup0 with later timestamp
+		// author0 joins privateGroup0 with wrong join message
 		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(),
+		GroupMessage joinMsg0 = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author0,
+						joinTime, getRandomBytes(12));
+		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
+		assertEquals(joinMsg0.getMessage().getId(),
 				groupManager0.getPreviousMsgId(groupId0));
 
 		// make group visible to 1
@@ -342,15 +337,21 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		t0.getDatabaseComponent().commitTransaction(txn0);
 		t0.getDatabaseComponent().endTransaction(txn0);
 
-		// author1 joins privateGroup0 and refers to wrong NEW_MEMBER message
-		joinMsg = groupMessageFactory
-				.createJoinMessage(groupId0, joinTime, author1,
-						newMemberMsg.getMessage().getId());
+		// author1 joins privateGroup0 with wrong timestamp
 		joinTime = clock.currentTimeMillis();
-		newMemberMsg = groupMessageFactory
-				.createNewMemberMessage(groupId0, joinTime, author0, author1);
-		groupManager1.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
-		assertEquals(joinMsg.getMessage().getId(),
+		long inviteTime = joinTime;
+		Group invitationGroup = contactGroupFactory
+				.createContactGroup(groupInvitationManager.getClientId(),
+						author0.getId(), author1.getId());
+		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
+				privateGroup0.getId());
+		byte[] creatorSignature =
+				clientHelper.sign(toSign, author0.getPrivateKey());
+		GroupMessage joinMsg1 = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
+						inviteTime, creatorSignature);
+		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
+		assertEquals(joinMsg1.getMessage().getId(),
 				groupManager1.getPreviousMsgId(groupId0));
 
 		// make group visible to 0
@@ -363,14 +364,73 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 
 		// 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();
+		validationWaiter.await(TIMEOUT, 1);
+
+		// assert that 1 never joined the group from 0's perspective
+		assertEquals(1, groupManager0.getHeaders(groupId0).size());
+	}
+
+	@Test
+	public void testWrongJoinMessageSignature() throws Exception {
+		addDefaultIdentities();
+		addDefaultContacts();
+		listenToEvents();
+
+		// author0 joins privateGroup0 properly
+		long joinTime = clock.currentTimeMillis();
+		GroupMessage joinMsg0 = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author0);
+		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
+		assertEquals(joinMsg0.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 with wrong timestamp
+		joinTime = clock.currentTimeMillis();
+		long inviteTime = joinTime - 1;
+		Group invitationGroup = contactGroupFactory
+				.createContactGroup(groupInvitationManager.getClientId(),
+						author0.getId(), author1.getId());
+		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
+				privateGroup0.getId());
+		byte[] creatorSignature =
+				clientHelper.sign(toSign, author1.getPrivateKey());
+		GroupMessage joinMsg1 = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
+						inviteTime, creatorSignature);
+		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
+		assertEquals(joinMsg1.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);
+
+		// assert that 0 never joined the group from 1's perspective
+		assertEquals(2, groupManager1.getHeaders(groupId0).size());
+
+		sync1To0();
 		validationWaiter.await(TIMEOUT, 1);
 
 		// assert that 1 never joined the group from 0's perspective
@@ -452,14 +512,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 	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(),
+		GroupMessage joinMsg0 = groupMessageFactory
+				.createJoinMessage(privateGroup0.getId(), joinTime, author0);
+		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
+		assertEquals(joinMsg0.getMessage().getId(),
 				groupManager0.getPreviousMsgId(groupId0));
 
 		// make group visible to 1
@@ -472,14 +528,19 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 
 		// author1 joins privateGroup0
 		joinTime = clock.currentTimeMillis();
-		GroupMessage newMemberMsg1 = groupMessageFactory
-				.createNewMemberMessage(privateGroup0.getId(), joinTime,
-						author0, author1);
-		joinMsg = groupMessageFactory
+		long inviteTime = joinTime - 1;
+		Group invitationGroup = contactGroupFactory
+				.createContactGroup(groupInvitationManager.getClientId(),
+						author0.getId(), author1.getId());
+		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
+				privateGroup0.getId());
+		byte[] creatorSignature =
+				clientHelper.sign(toSign, author0.getPrivateKey());
+		GroupMessage joinMsg1 = groupMessageFactory
 				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
-						newMemberMsg1.getMessage().getId());
-		groupManager1.addPrivateGroup(privateGroup0, newMemberMsg1, joinMsg);
-		assertEquals(joinMsg.getMessage().getId(),
+						inviteTime, creatorSignature);
+		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
+		assertEquals(joinMsg1.getMessage().getId(),
 				groupManager1.getPreviousMsgId(groupId0));
 
 		// make group visible to 0
@@ -492,9 +553,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 
 		// sync join messages
 		sync0To1();
-		deliveryWaiter.await(TIMEOUT, 2);
+		deliveryWaiter.await(TIMEOUT, 1);
 		sync1To0();
-		deliveryWaiter.await(TIMEOUT, 2);
+		deliveryWaiter.await(TIMEOUT, 1);
 	}
 
 	private void sync0To1() throws IOException, TimeoutException {
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 db661c43da5863f4b5a5805e7434ab88e2775e8b..b5632e9bf35d0aaa14cf2e40ae3bdea038ea2d61 100644
--- a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
@@ -82,29 +82,24 @@ public class CreateGroupControllerImpl extends DbControllerImpl
 				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);
+								clock.currentTimeMillis(), author);
+				storeGroup(group, joinMsg, handler);
 			}
 		});
 	}
 
 	private void storeGroup(final PrivateGroup group,
-			final GroupMessage newMemberMsg, final GroupMessage joinMsg,
+			final GroupMessage joinMsg,
 			final ResultExceptionHandler<GroupId, DbException> handler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
 				LOG.info("Adding group to database...");
 				try {
-					groupManager.addPrivateGroup(group, newMemberMsg, joinMsg);
+					groupManager.addPrivateGroup(group, joinMsg);
 					handler.onResult(group.getId());
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
diff --git a/briar-api/src/org/briarproject/api/clients/ContactGroupFactory.java b/briar-api/src/org/briarproject/api/clients/ContactGroupFactory.java
index 7043bb30d1d7e58eef0905d53063fb8e106fd1bd..705934de6db3e79626d7682275a5f9a2f459f92a 100644
--- a/briar-api/src/org/briarproject/api/clients/ContactGroupFactory.java
+++ b/briar-api/src/org/briarproject/api/clients/ContactGroupFactory.java
@@ -1,9 +1,9 @@
 package org.briarproject.api.clients;
 
 import org.briarproject.api.contact.Contact;
+import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupFactory;
 
 public interface ContactGroupFactory {
 
@@ -13,4 +13,11 @@ public interface ContactGroupFactory {
 	/** Creates a group for the given client to share with the given contact. */
 	Group createContactGroup(ClientId clientId, Contact contact);
 
+	/**
+	 * Creates a group for the given client to share between the given authors
+	 * identified by their AuthorIds.
+	 */
+	Group createContactGroup(ClientId clientId, AuthorId authorId1,
+			AuthorId authorId2);
+
 }
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
index 26e7ae9c7b73f533b5136fc05a709d28347e171d..3b50246958da22362be8afcb1b13558fc16f4dbb 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageFactory.java
@@ -10,33 +10,29 @@ import org.jetbrains.annotations.Nullable;
 public interface GroupMessageFactory {
 
 	/**
-	 * 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.
+	 * Creates a join announcement message for the creator of a 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
+	 * @param groupId     The ID of the Group that is being joined
+	 * @param timestamp   Must be greater than the timestamp of the invitation message
+	 * @param creator     The creator's LocalAuthor
 	 */
 	@CryptoExecutor
-	GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
-			LocalAuthor creator, Author member);
+	GroupMessage createJoinMessage(GroupId groupId, long timestamp,
+			LocalAuthor creator);
 
 	/**
-	 * Creates a join announcement message
-	 * that depends on a previous new member announcement.
+	 * Creates a join announcement message for a joining member.
 	 *
-	 * @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
+	 * @param groupId          The ID of the Group that is being joined
+	 * @param timestamp        Must be greater than the timestamp of the
+	 *                         invitation message
+	 * @param member           The member's LocalAuthor
+	 * @param inviteTimestamp  The timestamp of the group invitation message
+	 * @param creatorSignature The creator's signature from the group invitation
 	 */
 	@CryptoExecutor
 	GroupMessage createJoinMessage(GroupId groupId, long timestamp,
-			LocalAuthor member, MessageId newMemberId);
+			LocalAuthor member, long inviteTimestamp, byte[] creatorSignature);
 
 	/**
 	 * Creates a group message
diff --git a/briar-api/src/org/briarproject/api/privategroup/MessageType.java b/briar-api/src/org/briarproject/api/privategroup/MessageType.java
index 7cffb0df312973ac3b4955547037782aa7a294fe..8b5cf7d578dff9631d0a6e8dabe126b0b7a88932 100644
--- a/briar-api/src/org/briarproject/api/privategroup/MessageType.java
+++ b/briar-api/src/org/briarproject/api/privategroup/MessageType.java
@@ -1,9 +1,8 @@
 package org.briarproject.api.privategroup;
 
 public enum MessageType {
-	NEW_MEMBER(0),
-	JOIN(1),
-	POST(2);
+	JOIN(0),
+	POST(1);
 
 	int value;
 
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
index ded7a1702848a01cd4db0290fb6f8999766f16a5..d0ed0d7500d2d39de7dcadf91102af61db609f9b 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
@@ -21,12 +21,10 @@ public interface PrivateGroupManager extends MessageTracker {
 	 * 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;
+	void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
+			throws DbException;
 
 	/** Removes a dissolved private group. */
 	void removePrivateGroup(GroupId g) throws DbException;
diff --git a/briar-core/src/org/briarproject/clients/ContactGroupFactoryImpl.java b/briar-core/src/org/briarproject/clients/ContactGroupFactoryImpl.java
index d3a28dd99435f3869077ec84a4dd3e7d10698770..f5cbaf8e9d33d48d26d2b76f8ad2a11afe92e7a0 100644
--- a/briar-core/src/org/briarproject/clients/ContactGroupFactoryImpl.java
+++ b/briar-core/src/org/briarproject/clients/ContactGroupFactoryImpl.java
@@ -41,6 +41,13 @@ class ContactGroupFactoryImpl implements ContactGroupFactory {
 		return groupFactory.createGroup(clientId, descriptor);
 	}
 
+	@Override
+	public Group createContactGroup(ClientId clientId, AuthorId authorId1,
+			AuthorId authorId2) {
+		byte[] descriptor = createGroupDescriptor(authorId1, authorId2);
+		return groupFactory.createGroup(clientId, descriptor);
+	}
+
 	private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) {
 		try {
 			if (Bytes.COMPARATOR.compare(local, remote) < 0)
diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
index 228b6425448958ae1ebaee41e372160b8092a078..aeb478001b91dc4fe04aaaacc3da460664082f33 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageFactoryImpl.java
@@ -3,7 +3,6 @@ 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;
@@ -18,7 +17,6 @@ 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
@@ -32,45 +30,34 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
 	}
 
 	@Override
-	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);
+	public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
+			LocalAuthor creator) {
 
-			return new GroupMessage(m, null, member);
-		} catch (GeneralSecurityException e) {
-			throw new RuntimeException(e);
-		} catch (FormatException e) {
-			throw new RuntimeException(e);
-		}
+		return createJoinMessage(groupId, timestamp, creator, null);
 	}
 
 	@Override
 	public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
-			LocalAuthor member, MessageId newMemberId) {
+			LocalAuthor member, long inviteTimestamp, byte[] creatorSignature) {
+
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		return createJoinMessage(groupId, timestamp, member, invite);
+	}
+
+	private GroupMessage createJoinMessage(GroupId groupId, long timestamp,
+			LocalAuthor member, @Nullable BdfList invite) {
 		try {
 			// Generate the signature
 			int type = JOIN.getInt();
 			BdfList toSign = BdfList.of(groupId, timestamp, type,
-					member.getName(), member.getPublicKey(), newMemberId);
-			byte[] signature =
+					member.getName(), member.getPublicKey(), invite);
+			byte[] memberSignature =
 					clientHelper.sign(toSign, member.getPrivateKey());
 
 			// Compose the message
 			BdfList body =
 					BdfList.of(type, member.getName(),
-							member.getPublicKey(), newMemberId, signature);
+							member.getPublicKey(), invite, memberSignature);
 			Message m = clientHelper.createMessage(groupId, timestamp, body);
 
 			return new GroupMessage(m, null, member);
diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
index 25bf14730cc4b988b91728de14f9ef10fa22f2f7..cddbcd92997a9fc91c2bc79e6c2c249f525812b1 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
@@ -3,6 +3,7 @@ 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.clients.ContactGroupFactory;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
@@ -11,6 +12,7 @@ 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.privategroup.invitation.GroupInvitationManager;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
@@ -21,19 +23,16 @@ 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;
@@ -42,52 +41,50 @@ import static org.briarproject.privategroup.Constants.KEY_TYPE;
 
 class GroupMessageValidator extends BdfMessageValidator {
 
+	private final ContactGroupFactory contactGroupFactory;
 	private final PrivateGroupFactory groupFactory;
 	private final AuthorFactory authorFactory;
+	private final GroupInvitationManager groupInvitationManager; // TODO remove
 
-	GroupMessageValidator(PrivateGroupFactory groupFactory,
+	GroupMessageValidator(ContactGroupFactory contactGroupFactory,
+			PrivateGroupFactory groupFactory,
 			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
-			Clock clock, AuthorFactory authorFactory) {
+			Clock clock, AuthorFactory authorFactory,
+			GroupInvitationManager groupInvitationManager) {
 		super(clientHelper, metadataEncoder, clock);
+		this.contactGroupFactory = contactGroupFactory;
 		this.groupFactory = groupFactory;
 		this.authorFactory = authorFactory;
+		this.groupInvitationManager = groupInvitationManager;
 	}
 
 	@Override
 	protected BdfMessageContext validateMessage(Message m, Group g,
 			BdfList body) throws InvalidMessageException, FormatException {
 
-		checkSize(body, 4, 7);
+		checkSize(body, 5, 7);
 
 		// message type (int)
 		int type = body.getLong(0).intValue();
-		body.removeElementAt(0);
 
 		// member_name (string)
-		String memberName = body.getString(0);
+		String memberName = body.getString(1);
 		checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH);
 
 		// member_public_key (raw)
-		byte[] memberPublicKey = body.getRaw(1);
+		byte[] memberPublicKey = body.getRaw(2);
 		checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
 
+		Author member = authorFactory.createAuthor(memberName, memberPublicKey);
 		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());
+				c = validateJoin(m, g, body, member);
+				addMessageMetadata(c, member, m.getTimestamp());
 				break;
 			case POST:
-				c = validatePost(m, g, body, memberName, memberPublicKey);
-				addMessageMetadata(c, memberName, memberPublicKey,
-						m.getTimestamp());
+				c = validatePost(m, g, body, member);
+				addMessageMetadata(c, member, m.getTimestamp());
 				break;
 			default:
 				throw new InvalidMessageException("Unknown Message Type");
@@ -96,104 +93,106 @@ class GroupMessageValidator extends BdfMessageValidator {
 		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)
+			Author member)
 			throws InvalidMessageException, FormatException {
 
-		// The content is a BDF list with four elements
-		checkSize(body, 4);
+		// The content is a BDF list with five elements
+		checkSize(body, 5);
+		PrivateGroup pg = groupFactory.parsePrivateGroup(g);
+
+		// invite is null if the member is the creator of the private group
+		BdfList invite = body.getList(3, null);
+		if (invite == null) {
+			if (!member.equals(pg.getAuthor()))
+				throw new InvalidMessageException();
+		} else {
+			// Otherwise invite is a list with two elements
+			checkSize(invite, 2);
+
+			// invite_timestamp (int)
+			// join_timestamp must be greater than invite_timestamp
+			long inviteTimestamp = invite.getLong(0);
+			if (m.getTimestamp() <= inviteTimestamp)
+				throw new InvalidMessageException();
+
+			// creator_signature (raw)
+			byte[] creatorSignature = invite.getRaw(1);
+			checkLength(creatorSignature, 1, MAX_SIGNATURE_LENGTH);
+
+			// derive invitation group
+			Group invitationGroup = contactGroupFactory
+					.createContactGroup(groupInvitationManager.getClientId(),
+							pg.getAuthor().getId(), member.getId());
+
+			// signature with the creator's private key
+			// over a list with four elements:
+			// invite_type (int), invite_timestamp (int),
+			// invitation_group_id (raw), and private_group_id (raw)
+			BdfList signed =
+					BdfList.of(0, inviteTimestamp, invitationGroup.getId(),
+							g.getId());
+			try {
+				clientHelper.verifySignature(creatorSignature,
+						pg.getAuthor().getPublicKey(), signed);
+			} catch (GeneralSecurityException e) {
+				throw new InvalidMessageException(e);
+			}
+		}
 
-		// 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);
+		// member_signature (raw)
+		// a signature with the member's private key over a list with 6 elements
+		byte[] memberSignature = body.getRaw(4);
+		checkLength(memberSignature, 1, MAX_SIGNATURE_LENGTH);
 
 		// Verify Signature
 		BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
-				memberName, memberPublicKey, newMemberId);
+				member.getName(), member.getPublicKey(), invite);
 		try {
-			clientHelper.verifySignature(signature, memberPublicKey, signed);
+			clientHelper.verifySignature(memberSignature, member.getPublicKey(),
+					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
+		// Return the metadata and no dependencies
 		BdfDictionary meta = new BdfDictionary();
-		meta.put(KEY_NEW_MEMBER_MSG_ID, newMemberId);
-		return new BdfMessageContext(meta, dependencies);
+		return new BdfMessageContext(meta);
 	}
 
 	private BdfMessageContext validatePost(Message m, Group g, BdfList body,
-			String memberName, byte[] memberPublicKey)
+			Author member)
 			throws InvalidMessageException, FormatException {
 
 		// The content is a BDF list with six elements
-		checkSize(body, 6);
+		checkSize(body, 7);
 
 		// parent_id (raw or null)
 		// the identifier of the post to which this is a reply, if any
-		byte[] parentId = body.getOptionalRaw(2);
+		byte[] parentId = body.getOptionalRaw(3);
 		checkLength(parentId, MessageId.LENGTH);
 
 		// previous_message_id (raw)
 		// the identifier of the member's previous post or join message
-		byte[] previousMessageId = body.getRaw(3);
+		byte[] previousMessageId = body.getRaw(4);
 		checkLength(previousMessageId, MessageId.LENGTH);
 
 		// content (string)
-		String content = body.getString(4);
+		String content = body.getString(5);
 		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);
+		byte[] signature = body.getRaw(6);
 		checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
 
 		// Verify Signature
 		BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(),
-				memberName, memberPublicKey, parentId, previousMessageId,
-				content);
+				member.getName(), member.getPublicKey(), parentId,
+				previousMessageId, content);
 		try {
-			clientHelper.verifySignature(signature, memberPublicKey, signed);
+			clientHelper
+					.verifySignature(signature, member.getPublicKey(), signed);
 		} catch (GeneralSecurityException e) {
 			throw new InvalidMessageException(e);
 		}
@@ -211,14 +210,13 @@ class GroupMessageValidator extends BdfMessageValidator {
 		return new BdfMessageContext(meta, dependencies);
 	}
 
-	private void addMessageMetadata(BdfMessageContext c, String authorName,
-			byte[] pubKey, long time) {
+	private void addMessageMetadata(BdfMessageContext c, Author member,
+			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);
+		c.getDictionary().put(KEY_MEMBER_ID, member.getId());
+		c.getDictionary().put(KEY_MEMBER_NAME, member.getName());
+		c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, member.getPublicKey());
 	}
 
 }
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index e96c7edf4c4c494a6fbafee6d30dd711decf99b3..db4a73dc3786edd8e1ebc256c5fbf0307faa87ce 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -49,14 +49,12 @@ import static org.briarproject.api.identity.Author.Status.OURSELVES;
 import static org.briarproject.api.identity.Author.Status.UNVERIFIED;
 import static org.briarproject.api.identity.Author.Status.VERIFIED;
 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_DISSOLVED;
 import static org.briarproject.privategroup.Constants.KEY_MEMBERS;
 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;
@@ -95,8 +93,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 	}
 
 	@Override
-	public void addPrivateGroup(PrivateGroup group,
-			GroupMessage newMemberMsg, GroupMessage joinMsg)
+	public void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
 			throws DbException {
 		Transaction txn = db.startTransaction(false);
 		try {
@@ -106,7 +103,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 					new BdfEntry(KEY_DISSOLVED, false)
 			);
 			clientHelper.mergeGroupMetadata(txn, group.getId(), meta);
-			announceNewMember(txn, newMemberMsg);
 			joinPrivateGroup(txn, joinMsg);
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
@@ -116,14 +112,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		}
 	}
 
-	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();
@@ -315,8 +303,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 			// 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));
 			}
@@ -328,8 +314,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 			// 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));
 			}
@@ -434,36 +418,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		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;
-				}
 				addMember(txn, m.getGroupId(), getAuthor(meta));
 				trackIncomingMessage(txn, m);
 				return true;
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
index 49c0714c5cb0987c2fd5c602578ec498e42efdf0..2c60d72073c52fa01445c4a14dc888f4c45f7498 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java
@@ -1,6 +1,7 @@
 package org.briarproject.privategroup;
 
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.ContactGroupFactory;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.AuthorFactory;
@@ -58,14 +59,16 @@ public class PrivateGroupModule {
 	@Provides
 	@Singleton
 	GroupMessageValidator provideGroupMessageValidator(
+			ContactGroupFactory contactGroupFactory,
 			PrivateGroupFactory groupFactory,
 			ValidationManager validationManager, ClientHelper clientHelper,
 			MetadataEncoder metadataEncoder, Clock clock,
-			AuthorFactory authorFactory) {
+			AuthorFactory authorFactory,
+			GroupInvitationManager groupInvitationManager) {
 
 		GroupMessageValidator validator = new GroupMessageValidator(
-				groupFactory, clientHelper, metadataEncoder, clock,
-				authorFactory);
+				contactGroupFactory, groupFactory, clientHelper,
+				metadataEncoder, clock, authorFactory, groupInvitationManager);
 		validationManager.registerMessageValidator(
 				PrivateGroupManagerImpl.CLIENT_ID, validator);