diff --git a/api/net/sf/briar/api/ContactId.java b/api/net/sf/briar/api/ContactId.java
index 0dbdc140fbd7b8d98a0c056704b5853db7898660..d8a9f7a388c4ab7f1df97bec91c0c61766670e8b 100644
--- a/api/net/sf/briar/api/ContactId.java
+++ b/api/net/sf/briar/api/ContactId.java
@@ -1,6 +1,9 @@
 package net.sf.briar.api;
 
-/** Type-safe wrapper for an integer that uniquely identifies a contact. */
+/**
+ * Type-safe wrapper for an integer that uniquely identifies a contact within
+ * the scope of a single node.
+ */
 public class ContactId {
 
 	private final int id;
@@ -23,9 +26,4 @@ public class ContactId {
 	public int hashCode() {
 		return id;
 	}
-
-	@Override
-	public String toString() {
-		return String.valueOf(id);
-	}
 }
diff --git a/api/net/sf/briar/api/TransportId.java b/api/net/sf/briar/api/TransportId.java
deleted file mode 100644
index 591ba0769cdc84ee26f8d4bd8347fc1949a6576a..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/TransportId.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.sf.briar.api;
-
-import java.io.IOException;
-
-import net.sf.briar.api.serial.Writable;
-import net.sf.briar.api.serial.Writer;
-
-/**
- * Type-safe wrapper for an integer that uniquely identifies a transport plugin.
- */
-public class TransportId implements Writable {
-
-	public static final int MIN_ID = 0;
-	public static final int MAX_ID = 65535;
-
-	private final int id;
-
-	public TransportId(int id) {
-		if(id < MIN_ID || id > MAX_ID) throw new IllegalArgumentException();
-		this.id = id;
-	}
-
-	public int getInt() {
-		return id;
-	}
-
-	public void writeTo(Writer w) throws IOException {
-		w.writeInt32(id);
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if(o instanceof TransportId) return id == ((TransportId) o).id;
-		return false;
-	}
-
-	@Override
-	public int hashCode() {
-		return id;
-	}
-}
diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 4d50c4cc5c656ff93c1b684e53306575e1e4c969..1e1402b1f1afd7c479ef30cd27f4b1222235e218 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -7,7 +7,6 @@ import java.util.Map;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.protocol.Ack;
@@ -19,6 +18,9 @@ import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
@@ -51,11 +53,10 @@ public interface DatabaseComponent {
 	void removeListener(DatabaseListener d);
 
 	/**
-	 * Adds a new contact to the database with the given transport properties
-	 * and shared secret, returns an ID for the contact.
+	 * Adds a new contact to the database with the given secret and returns an
+	 * ID for the contact.
 	 */
-	ContactId addContact(Map<TransportId, TransportProperties> transports,
-			byte[] secret) throws DbException;
+	ContactId addContact(byte[] secret) throws DbException;
 
 	/** Adds a locally generated group message to the database. */
 	void addLocalGroupMessage(Message m) throws DbException;
@@ -63,6 +64,12 @@ public interface DatabaseComponent {
 	/** Adds a locally generated private message to the database. */
 	void addLocalPrivateMessage(Message m, ContactId c) throws DbException;
 
+	/**
+	 * Allocates and returns a local index for the given transport. Returns
+	 * null if all indices have been allocated.
+	 */
+	TransportIndex addTransport(TransportId t) throws DbException;
+
 	/**
 	 * Generates an acknowledgement for the given contact.
 	 * @return True if any batch IDs were added to the acknowledgement.
@@ -109,24 +116,29 @@ public interface DatabaseComponent {
 	 * Returns an outgoing connection number for the given contact and
 	 * transport.
 	 */
-	long getConnectionNumber(ContactId c, TransportId t) throws DbException;
+	long getConnectionNumber(ContactId c, TransportIndex i) throws DbException;
 
 	/**
 	 * Returns the connection reordering window for the given contact and
 	 * transport.
 	 */
-	ConnectionWindow getConnectionWindow(ContactId c, TransportId t)
+	ConnectionWindow getConnectionWindow(ContactId c, TransportIndex i)
 	throws DbException;
 
 	/** Returns the IDs of all contacts. */
 	Collection<ContactId> getContacts() throws DbException;
 
+	/**
+	 * Returns the local index for the given transport, or null if no index
+	 * has been allocated.
+	 */
+	TransportIndex getLocalIndex(TransportId t) throws DbException;
+
 	/** Returns the local transport properties for the given transport. */
 	TransportProperties getLocalProperties(TransportId t) throws DbException;
 
-	/** Returns all local transport properties. */
-	Map<TransportId, TransportProperties> getLocalTransports()
-	throws DbException;
+	/** Returns all local transports. */
+	Collection<Transport> getLocalTransports() throws DbException;
 
 	/** Returns the headers of all messages in the given group. */
 	Collection<MessageHeader> getMessageHeaders(GroupId g) throws DbException;
@@ -134,6 +146,13 @@ public interface DatabaseComponent {
 	/** Returns the user's rating for the given author. */
 	Rating getRating(AuthorId a) throws DbException;
 
+	/**
+	 * Returns the given contact's index for the given transport, or null if
+	 * the contact does not support the transport.
+	 */
+	TransportIndex getRemoteIndex(ContactId c, TransportId t)
+	throws DbException;
+
 	/** Returns all remote transport properties for the given transport. */
 	Map<ContactId, TransportProperties> getRemoteProperties(TransportId t)
 	throws DbException;
@@ -191,8 +210,8 @@ public interface DatabaseComponent {
 	 * Sets the connection reordering window for the given contact and
 	 * transport.
 	 */
-	void setConnectionWindow(ContactId c, TransportId t, ConnectionWindow w)
-	throws DbException;
+	void setConnectionWindow(ContactId c, TransportIndex i,
+			ConnectionWindow w) throws DbException;
 
 	/**
 	 * Sets the local transport properties for the given transport, replacing
diff --git a/api/net/sf/briar/api/db/event/ContactRemovedEvent.java b/api/net/sf/briar/api/db/event/ContactRemovedEvent.java
index face9f4e6d1eae72d19280ef3e9f867e5d26506a..74eed602994e691912f2f385d6d8a666b65c97bb 100644
--- a/api/net/sf/briar/api/db/event/ContactRemovedEvent.java
+++ b/api/net/sf/briar/api/db/event/ContactRemovedEvent.java
@@ -3,9 +3,15 @@ package net.sf.briar.api.db.event;
 import net.sf.briar.api.ContactId;
 
 /** An event that is broadcast when a contact is removed. */
-public class ContactRemovedEvent extends ContactAddedEvent {
+public class ContactRemovedEvent extends DatabaseEvent {
+
+	private final ContactId contactId;
 
 	public ContactRemovedEvent(ContactId contactId) {
-		super(contactId);
+		this.contactId = contactId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
 	}
 }
diff --git a/api/net/sf/briar/api/db/event/LocalTransportsUpdatedEvent.java b/api/net/sf/briar/api/db/event/LocalTransportsUpdatedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..c60c23b83b1000df6d6b7d933bbb8f2822abfb2e
--- /dev/null
+++ b/api/net/sf/briar/api/db/event/LocalTransportsUpdatedEvent.java
@@ -0,0 +1,9 @@
+package net.sf.briar.api.db.event;
+
+/**
+ * An event that is broadcast when the local transport properties are
+ * updated.
+ */
+public class LocalTransportsUpdatedEvent extends DatabaseEvent {
+
+}
diff --git a/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..8418d235951e7abdb99b28ebd1d2297fa0a1d263
--- /dev/null
+++ b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.ContactId;
+
+/** An event that is broadcast when a contact's transports are updated. */
+public class RemoteTransportsUpdatedEvent extends DatabaseEvent {
+
+	private final ContactId contactId;
+
+	public RemoteTransportsUpdatedEvent(ContactId contactId) {
+		this.contactId = contactId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+}
diff --git a/api/net/sf/briar/api/db/event/TransportAddedEvent.java b/api/net/sf/briar/api/db/event/TransportAddedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..75156882292478155b0ef2b3c29e30511c5e6f8b
--- /dev/null
+++ b/api/net/sf/briar/api/db/event/TransportAddedEvent.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.db.event;
+
+import net.sf.briar.api.protocol.TransportId;
+
+/** An event that is broadcast when a transport is added. */
+public class TransportAddedEvent extends DatabaseEvent {
+
+	private final TransportId transportId;
+
+	public TransportAddedEvent(TransportId transportId) {
+		this.transportId = transportId;
+	}
+
+	public TransportId getTransportId() {
+		return transportId;
+	}
+}
diff --git a/api/net/sf/briar/api/db/event/TransportsUpdatedEvent.java b/api/net/sf/briar/api/db/event/TransportsUpdatedEvent.java
deleted file mode 100644
index 1360fe9ace25847ed97332eac6f22a68ee5df431..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/db/event/TransportsUpdatedEvent.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.sf.briar.api.db.event;
-
-/** An event that is broadcast when the local transports are updated. */
-public class TransportsUpdatedEvent extends DatabaseEvent {
-
-}
diff --git a/api/net/sf/briar/api/plugins/BatchPluginFactory.java b/api/net/sf/briar/api/plugins/BatchPluginFactory.java
index e8c665b216b6d248a5f233f5eee96e36bda48f62..1a6d6a0799058a4a24481dc8d886b2f3bef5141c 100644
--- a/api/net/sf/briar/api/plugins/BatchPluginFactory.java
+++ b/api/net/sf/briar/api/plugins/BatchPluginFactory.java
@@ -4,6 +4,5 @@ import java.util.concurrent.Executor;
 
 public interface BatchPluginFactory {
 
-	BatchPlugin createPlugin(Executor executor,
-			BatchPluginCallback callback);
+	BatchPlugin createPlugin(Executor executor, BatchPluginCallback callback);
 }
diff --git a/api/net/sf/briar/api/plugins/Plugin.java b/api/net/sf/briar/api/plugins/Plugin.java
index 3358c1ff6a31ee1c7e37310c3df57cb73f90c3bf..6d476087ca1cc63877cd86f8c057b551e3624e53 100644
--- a/api/net/sf/briar/api/plugins/Plugin.java
+++ b/api/net/sf/briar/api/plugins/Plugin.java
@@ -2,7 +2,7 @@ package net.sf.briar.api.plugins;
 
 import java.io.IOException;
 
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportId;
 
 public interface Plugin {
 
diff --git a/api/net/sf/briar/api/plugins/PluginManager.java b/api/net/sf/briar/api/plugins/PluginManager.java
index 379c07bb977315c5060029491c4eb453df12feeb..2fae4b17c8789b8d315d2d67fbfc8f495ae4608a 100644
--- a/api/net/sf/briar/api/plugins/PluginManager.java
+++ b/api/net/sf/briar/api/plugins/PluginManager.java
@@ -3,14 +3,13 @@ package net.sf.briar.api.plugins;
 public interface PluginManager {
 
 	/**
-	 * Starts all the plugins the manager knows about and returns the number of
-	 * plugins successfully started.
+	 * Starts the plugins and returns the number of plugins successfully
+	 * started.
 	 */
 	int startPlugins();
 
 	/**
-	 * Stops all the plugins started by startPlugins() and returns the number
-	 * of plugins successfully stopped.
+	 * Stops the plugins and returns the number of plugins successfully stopped.
 	 */
 	int stopPlugins();
 }
diff --git a/api/net/sf/briar/api/plugins/StreamPluginFactory.java b/api/net/sf/briar/api/plugins/StreamPluginFactory.java
index 97246ffc5ebc0f2ac99bfe041053d22b9b65da8b..f8a0388b70ad44184b37990eb1a38f3ac641ec2e 100644
--- a/api/net/sf/briar/api/plugins/StreamPluginFactory.java
+++ b/api/net/sf/briar/api/plugins/StreamPluginFactory.java
@@ -4,6 +4,5 @@ import java.util.concurrent.Executor;
 
 public interface StreamPluginFactory {
 
-	StreamPlugin createPlugin(Executor executor,
-			StreamPluginCallback callback);
+	StreamPlugin createPlugin(Executor executor, StreamPluginCallback callback);
 }
diff --git a/api/net/sf/briar/api/protocol/Author.java b/api/net/sf/briar/api/protocol/Author.java
index 2c72bc4986ebf1912277194f6a419e44d01db95a..52266b9621ca4de098d7bb10f50ab48412b399f1 100644
--- a/api/net/sf/briar/api/protocol/Author.java
+++ b/api/net/sf/briar/api/protocol/Author.java
@@ -1,15 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import net.sf.briar.api.serial.Writable;
-
 /** A pseudonymous author of messages. */
-public interface Author extends Writable {
-
-	/** The maximum length of an author's name in UTF-8 bytes. */
-	static final int MAX_NAME_LENGTH = 50;
-
-	/** The maximum length of an author's public key in bytes. */
-	static final int MAX_PUBLIC_KEY_LENGTH = 100;
+public interface Author {
 
 	/** Returns the author's unique identifier. */
 	AuthorId getId();
diff --git a/api/net/sf/briar/api/protocol/AuthorId.java b/api/net/sf/briar/api/protocol/AuthorId.java
index 9cefbfc5f5a2e4f0e539c14f4c0a9053353c1d18..578c89c2accf4374de24d59bfcd7541808d79f24 100644
--- a/api/net/sf/briar/api/protocol/AuthorId.java
+++ b/api/net/sf/briar/api/protocol/AuthorId.java
@@ -1,10 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import java.io.IOException;
 import java.util.Arrays;
 
-import net.sf.briar.api.serial.Writer;
-
 /** Type-safe wrapper for a byte array that uniquely identifies an author. */
 public class AuthorId extends UniqueId {
 
@@ -12,11 +9,6 @@ public class AuthorId extends UniqueId {
 		super(id);
 	}
 
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.AUTHOR_ID);
-		w.writeBytes(id);
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof AuthorId)
diff --git a/api/net/sf/briar/api/protocol/BatchId.java b/api/net/sf/briar/api/protocol/BatchId.java
index a3d573bc9b2f559dce8e2c007fee9646618ae3ac..ab33800761d9558416896dde3862f3c69d83793e 100644
--- a/api/net/sf/briar/api/protocol/BatchId.java
+++ b/api/net/sf/briar/api/protocol/BatchId.java
@@ -1,10 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import java.io.IOException;
 import java.util.Arrays;
 
-import net.sf.briar.api.serial.Writer;
-
 /**
  * Type-safe wrapper for a byte array that uniquely identifies a batch of
  * messages.
@@ -15,11 +12,6 @@ public class BatchId extends UniqueId {
 		super(id);
 	}
 
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.BATCH_ID);
-		w.writeBytes(id);
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof BatchId)
diff --git a/api/net/sf/briar/api/protocol/Group.java b/api/net/sf/briar/api/protocol/Group.java
index 587bc72869b90091054e3633e83a068626ffd4a0..cc1f6ccf07a313240adc2c5ad4ffd43573d2f7df 100644
--- a/api/net/sf/briar/api/protocol/Group.java
+++ b/api/net/sf/briar/api/protocol/Group.java
@@ -1,15 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import net.sf.briar.api.serial.Writable;
-
 /** A group to which users may subscribe. */
-public interface Group extends Writable {
-
-	/** The maximum length of a group's name in UTF-8 bytes. */
-	static final int MAX_NAME_LENGTH = 50;
-
-	/** The maximum length of a group's public key in bytes. */
-	static final int MAX_PUBLIC_KEY_LENGTH = 100;
+public interface Group {
 
 	/** Returns the group's unique identifier. */
 	GroupId getId();
diff --git a/api/net/sf/briar/api/protocol/GroupId.java b/api/net/sf/briar/api/protocol/GroupId.java
index 0ea39e3b0fc6e58f2cdc1b6d94528d766d971701..532941867942b4bb02f4e8a6ed828820c7cbb20f 100644
--- a/api/net/sf/briar/api/protocol/GroupId.java
+++ b/api/net/sf/briar/api/protocol/GroupId.java
@@ -1,10 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import java.io.IOException;
 import java.util.Arrays;
 
-import net.sf.briar.api.serial.Writer;
-
 /**
  * Type-safe wrapper for a byte array that uniquely identifies a group to which
  * users may subscribe.
@@ -15,11 +12,6 @@ public class GroupId extends UniqueId {
 		super(id);
 	}
 
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.GROUP_ID);
-		w.writeBytes(id);
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof GroupId)
diff --git a/api/net/sf/briar/api/protocol/Message.java b/api/net/sf/briar/api/protocol/Message.java
index d204fd667f1d9673e4e394e5f688061f274cf837..e4b702fc7385d84c40fd2702f2aa60bbaf274c9e 100644
--- a/api/net/sf/briar/api/protocol/Message.java
+++ b/api/net/sf/briar/api/protocol/Message.java
@@ -2,23 +2,6 @@ package net.sf.briar.api.protocol;
 
 public interface Message {
 
-	/**
-	 * The maximum length of a message body in bytes. To allow for future
-	 * changes in the protocol, this is smaller than the maximum packet length
-	 * even when all the message's other fields have their maximum lengths.
-	 */
-	static final int MAX_BODY_LENGTH =
-		ProtocolConstants.MAX_PACKET_LENGTH - 1024;
-
-	/** The maximum length of a subject line in UTF-8 bytes. */
-	static final int MAX_SUBJECT_LENGTH = 100;
-
-	/** The maximum length of a signature in bytes. */
-	static final int MAX_SIGNATURE_LENGTH = 100;
-
-	/** The length of the random salt in bytes. */
-	static final int SALT_LENGTH = 8;
-
 	/** Returns the message's unique identifier. */
 	MessageId getId();
 
diff --git a/api/net/sf/briar/api/protocol/MessageId.java b/api/net/sf/briar/api/protocol/MessageId.java
index 5f1bd67c0477982e968468d4c5ba03585d0c55c4..e540f769ea820de7abd40dbb776075af264be3dd 100644
--- a/api/net/sf/briar/api/protocol/MessageId.java
+++ b/api/net/sf/briar/api/protocol/MessageId.java
@@ -1,10 +1,7 @@
 package net.sf.briar.api.protocol;
 
-import java.io.IOException;
 import java.util.Arrays;
 
-import net.sf.briar.api.serial.Writer;
-
 /** Type-safe wrapper for a byte array that uniquely identifies a message. */
 public class MessageId extends UniqueId {
 
@@ -12,11 +9,6 @@ public class MessageId extends UniqueId {
 		super(id);
 	}
 
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.MESSAGE_ID);
-		w.writeBytes(id);
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof MessageId)
diff --git a/api/net/sf/briar/api/protocol/ProtocolConstants.java b/api/net/sf/briar/api/protocol/ProtocolConstants.java
index a84f20e572fcdbb9de8de0ca118f16bb84ed899e..29f31df89975b9698beedc2a1c06dd514578854c 100644
--- a/api/net/sf/briar/api/protocol/ProtocolConstants.java
+++ b/api/net/sf/briar/api/protocol/ProtocolConstants.java
@@ -11,4 +11,41 @@ public interface ProtocolConstants {
 	 */
 	static final int MAX_PACKET_LENGTH =
 		TransportConstants.MIN_CONNECTION_LENGTH - 1024;
+
+	/** The maximum number of transport plugins a node may support. */
+	static final int MAX_TRANSPORTS = 50;
+
+	/** The maximum number of properties per transport plugin. */
+	static final int MAX_PROPERTIES_PER_TRANSPORT = 100;
+
+	/** The maximum length of a property's key or value in UTF-8 bytes. */
+	static final int MAX_PROPERTY_LENGTH = 100;
+
+	/** The maximum number of groups a node may subscribe to. */
+	static final int MAX_GROUPS = 6000;
+
+	/** The maximum length of a group's name in UTF-8 bytes. */
+	static final int MAX_GROUP_NAME_LENGTH = 50;
+
+	/** The maximum length of a serialised public key in bytes. */
+	static final int MAX_PUBLIC_KEY_LENGTH = 100;
+
+	/** The maximum length of an author's name in UTF-8 bytes. */
+	static final int MAX_AUTHOR_NAME_LENGTH = 50;
+
+	/**
+	 * The maximum length of a message body in bytes. To allow for future
+	 * changes in the protocol, this is smaller than the maximum packet length
+	 * even when all the message's other fields have their maximum lengths.
+	 */
+	static final int MAX_BODY_LENGTH = MAX_PACKET_LENGTH - 1024;
+
+	/** The maximum length of a message's subject line in UTF-8 bytes. */
+	static final int MAX_SUBJECT_LENGTH = 100;
+
+	/** The maximum length of a signature in bytes. */
+	static final int MAX_SIGNATURE_LENGTH = 100;
+
+	/** The length of a message's random salt in bytes. */
+	static final int SALT_LENGTH = 8;
 }
diff --git a/api/net/sf/briar/api/protocol/SubscriptionUpdate.java b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
index 31e2f4672f4b31b303700da7d479d8c0fd150d1d..05a17ed54ad1385071da215ed3fdd30c2e99ba3c 100644
--- a/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
+++ b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
@@ -5,9 +5,6 @@ import java.util.Map;
 /** A packet updating the sender's subscriptions. */
 public interface SubscriptionUpdate {
 
-	/** The maximum number of subscriptions per update. */
-	static final int MAX_SUBS_PER_UPDATE = 6000;
-
 	/** Returns the subscriptions contained in the update. */
 	Map<Group, Long> getSubscriptions();
 
diff --git a/api/net/sf/briar/api/protocol/Transport.java b/api/net/sf/briar/api/protocol/Transport.java
new file mode 100644
index 0000000000000000000000000000000000000000..37c9ab021c65e35050ece282d044820e4bd4bbfb
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/Transport.java
@@ -0,0 +1,47 @@
+package net.sf.briar.api.protocol;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+public class Transport extends TreeMap<String, String> {
+
+	private static final long serialVersionUID = 4900420175715429560L;
+
+	private final TransportId id;
+	private final TransportIndex index;
+
+	public Transport(TransportId id, TransportIndex index,
+			Map<String, String> p) {
+		super(p);
+		this.id = id;
+		this.index = index;
+	}
+
+	public Transport(TransportId id, TransportIndex index) {
+		super();
+		this.id = id;
+		this.index = index;
+	}
+
+	public TransportId getId() {
+		return id;
+	}
+
+	public TransportIndex getIndex() {
+		return index;
+	}
+
+	@Override
+	public int hashCode() {
+		return id.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if(o instanceof Transport) {
+			Transport t = (Transport) o;
+			return id.equals(t.id) && index.equals(t.index) && super.equals(o);
+		}
+		return false;
+	}
+}
diff --git a/api/net/sf/briar/api/protocol/TransportId.java b/api/net/sf/briar/api/protocol/TransportId.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f295683dd9b11ee685149a3a56296800d5a08a7
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/TransportId.java
@@ -0,0 +1,21 @@
+package net.sf.briar.api.protocol;
+
+import java.util.Arrays;
+
+/**
+ * Type-safe wrapper for a byte array that uniquely identifies a transport
+ * plugin.
+ */
+public class TransportId extends UniqueId {
+
+	public TransportId(byte[] id) {
+		super(id);
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if(o instanceof TransportId)
+			return Arrays.equals(id, ((TransportId) o).id);
+		return false;
+	}
+}
diff --git a/api/net/sf/briar/api/protocol/TransportIndex.java b/api/net/sf/briar/api/protocol/TransportIndex.java
new file mode 100644
index 0000000000000000000000000000000000000000..c167b49c69ac23498bea7870aae293d216a2674d
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/TransportIndex.java
@@ -0,0 +1,33 @@
+package net.sf.briar.api.protocol;
+
+
+/**
+ * Type-safe wrapper for an integer that uniquely identifies a transport plugin
+ * within the scope of a single node.
+ */
+public class TransportIndex {
+
+	private final int index;
+
+	public TransportIndex(int index) {
+		if(index < 0 || index >= ProtocolConstants.MAX_TRANSPORTS)
+			throw new IllegalArgumentException();
+		this.index = index;
+	}
+
+	public int getInt() {
+		return index;
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if(o instanceof TransportIndex)
+			return index == ((TransportIndex) o).index;
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return index;
+	}
+}
diff --git a/api/net/sf/briar/api/protocol/TransportUpdate.java b/api/net/sf/briar/api/protocol/TransportUpdate.java
index c9ec131f8d9f381213d4d21a3a6b5e9875529558..48615ccd7e72531f730fad5fce3492333d68f569 100644
--- a/api/net/sf/briar/api/protocol/TransportUpdate.java
+++ b/api/net/sf/briar/api/protocol/TransportUpdate.java
@@ -1,24 +1,12 @@
 package net.sf.briar.api.protocol;
 
-import java.util.Map;
-
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
+import java.util.Collection;
 
 /** A packet updating the sender's transport properties. */
 public interface TransportUpdate {
 
-	/** The maximum length of a property's key or value in UTF-8 bytes. */
-	static final int MAX_KEY_OR_VALUE_LENGTH = 100;
-
-	/** The maximum number of properties per plugin. */
-	static final int MAX_PROPERTIES_PER_PLUGIN = 100;
-
-	/** The maximum number of plugins per update. */
-	static final int MAX_PLUGINS_PER_UPDATE = 50;
-
-	/** Returns the transport properties contained in the update. */
-	Map<TransportId, TransportProperties> getTransports();
+	/** Returns the transports contained in the update. */
+	Collection<Transport> getTransports();
 
 	/**
 	 * Returns the update's timestamp. Updates that are older than the newest
diff --git a/api/net/sf/briar/api/protocol/Types.java b/api/net/sf/briar/api/protocol/Types.java
index 54dc03d9689b7ea70bb221f66c544b3e9c719b05..8ace5c0d9eb8a5a844d9a7b9a81f92e7676e012a 100644
--- a/api/net/sf/briar/api/protocol/Types.java
+++ b/api/net/sf/briar/api/protocol/Types.java
@@ -5,16 +5,14 @@ public interface Types {
 
 	static final int ACK = 0;
 	static final int AUTHOR = 1;
-	static final int AUTHOR_ID = 2;
-	static final int BATCH = 3;
-	static final int BATCH_ID = 4;
-	static final int GROUP = 5;
-	static final int GROUP_ID = 6;
-	static final int MESSAGE = 7;
-	static final int MESSAGE_ID = 8;
-	static final int OFFER = 9;
-	static final int REQUEST = 10;
-	static final int SUBSCRIPTION_UPDATE = 11;
-	static final int TRANSPORT_PROPERTIES = 12;
-	static final int TRANSPORT_UPDATE = 13;
+	static final int BATCH = 2;
+	static final int BATCH_ID = 3;
+	static final int GROUP = 4;
+	static final int MESSAGE = 5;
+	static final int MESSAGE_ID = 6;
+	static final int OFFER = 7;
+	static final int REQUEST = 8;
+	static final int SUBSCRIPTION_UPDATE = 9;
+	static final int TRANSPORT = 10;
+	static final int TRANSPORT_UPDATE = 11;
 }
diff --git a/api/net/sf/briar/api/protocol/UniqueId.java b/api/net/sf/briar/api/protocol/UniqueId.java
index cb379b89a5a0065a84c06231cfafd4667b3c60b1..8124ed03f0e930c70d4955c2ac23475a0bf7b5a4 100644
--- a/api/net/sf/briar/api/protocol/UniqueId.java
+++ b/api/net/sf/briar/api/protocol/UniqueId.java
@@ -2,9 +2,7 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
-import net.sf.briar.api.serial.Writable;
-
-public abstract class UniqueId implements Writable {
+public abstract class UniqueId {
 
 	/** The length of a unique identifier in bytes. */
 	public static final int LENGTH = 32;
diff --git a/api/net/sf/briar/api/protocol/writers/AuthorWriter.java b/api/net/sf/briar/api/protocol/writers/AuthorWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..702b0963ced259889e1dd1f5757bb3232b4e35d4
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/writers/AuthorWriter.java
@@ -0,0 +1,11 @@
+package net.sf.briar.api.protocol.writers;
+
+import java.io.IOException;
+
+import net.sf.briar.api.protocol.Author;
+import net.sf.briar.api.serial.Writer;
+
+public interface AuthorWriter {
+
+	void writeAuthor(Writer w, Author a) throws IOException;
+}
diff --git a/api/net/sf/briar/api/protocol/writers/GroupWriter.java b/api/net/sf/briar/api/protocol/writers/GroupWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..40d595b1a7aa77a9530757f73b704016d8ed0b03
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/writers/GroupWriter.java
@@ -0,0 +1,11 @@
+package net.sf.briar.api.protocol.writers;
+
+import java.io.IOException;
+
+import net.sf.briar.api.protocol.Group;
+import net.sf.briar.api.serial.Writer;
+
+public interface GroupWriter {
+
+	void writeGroup(Writer w, Group g) throws IOException;
+}
diff --git a/api/net/sf/briar/api/protocol/MessageEncoder.java b/api/net/sf/briar/api/protocol/writers/MessageEncoder.java
similarity index 85%
rename from api/net/sf/briar/api/protocol/MessageEncoder.java
rename to api/net/sf/briar/api/protocol/writers/MessageEncoder.java
index d01f5f0a9dc57739a91c2ca7883693a870070160..bb144720efc2e189d805b1354e54eb73f97dc8b9 100644
--- a/api/net/sf/briar/api/protocol/MessageEncoder.java
+++ b/api/net/sf/briar/api/protocol/writers/MessageEncoder.java
@@ -1,9 +1,14 @@
-package net.sf.briar.api.protocol;
+package net.sf.briar.api.protocol.writers;
 
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.PrivateKey;
 
+import net.sf.briar.api.protocol.Author;
+import net.sf.briar.api.protocol.Group;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageId;
+
 public interface MessageEncoder {
 
 	/** Encodes a private message. */
diff --git a/api/net/sf/briar/api/protocol/writers/TransportWriter.java b/api/net/sf/briar/api/protocol/writers/TransportWriter.java
index 2f19ad31d9b5b35411202ed4e441fad0fd065f64..fb5a88bffe6f74fd77a3ba19908995eac1e11e7d 100644
--- a/api/net/sf/briar/api/protocol/writers/TransportWriter.java
+++ b/api/net/sf/briar/api/protocol/writers/TransportWriter.java
@@ -1,15 +1,14 @@
 package net.sf.briar.api.protocol.writers;
 
 import java.io.IOException;
-import java.util.Map;
+import java.util.Collection;
 
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.protocol.Transport;
 
 /** An interface for creating a transport update. */
 public interface TransportWriter {
 
 	/** Writes the contents of the update. */
-	void writeTransports(Map<TransportId, TransportProperties> transports,
-			long timestamp) throws IOException;
+	void writeTransports(Collection<Transport> transports, long timestamp)
+	throws IOException;
 }
diff --git a/api/net/sf/briar/api/serial/Writable.java b/api/net/sf/briar/api/serial/Writable.java
deleted file mode 100644
index 7e75aa54351d4dfbed19c8813c8ac417b83784eb..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/serial/Writable.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.sf.briar.api.serial;
-
-import java.io.IOException;
-
-public interface Writable {
-
-	void writeTo(Writer w) throws IOException;
-}
diff --git a/api/net/sf/briar/api/transport/BatchConnectionFactory.java b/api/net/sf/briar/api/transport/BatchConnectionFactory.java
index 622a02d7722814b9a4d22f30988b6a99f8a2994c..20fb14e60bac0e0b2778eecad776a1c28bf1fb2d 100644
--- a/api/net/sf/briar/api/transport/BatchConnectionFactory.java
+++ b/api/net/sf/briar/api/transport/BatchConnectionFactory.java
@@ -1,13 +1,13 @@
 package net.sf.briar.api.transport;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 
 public interface BatchConnectionFactory {
 
-	void createIncomingConnection(TransportId t, ContactId c,
+	void createIncomingConnection(TransportIndex i, ContactId c,
 			BatchTransportReader r, byte[] encryptedIv);
 
-	void createOutgoingConnection(TransportId t, ContactId c,
+	void createOutgoingConnection(TransportIndex i, ContactId c,
 			BatchTransportWriter w);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionContext.java b/api/net/sf/briar/api/transport/ConnectionContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..692d69b65c16fb362641b33bc4a5d228406ebe01
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionContext.java
@@ -0,0 +1,16 @@
+package net.sf.briar.api.transport;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+
+public interface ConnectionContext {
+
+	ContactId getContactId();
+
+	TransportId getTransportId();
+
+	TransportIndex getTransportIndex();
+
+	long getConnectionNumber();
+}
diff --git a/api/net/sf/briar/api/transport/ConnectionDispatcher.java b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
index 9d21d65bf951a4b8fa1095f769c20e4c09fa6368..6207063d5a18849eabdad4367231edb5a920e411 100644
--- a/api/net/sf/briar/api/transport/ConnectionDispatcher.java
+++ b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
@@ -1,17 +1,17 @@
 package net.sf.briar.api.transport;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 
 public interface ConnectionDispatcher {
 
 	void dispatchReader(TransportId t, BatchTransportReader r);
 
-	void dispatchWriter(TransportId t, ContactId c,
-			BatchTransportWriter w);
+	void dispatchWriter(TransportIndex i, ContactId c, BatchTransportWriter w);
 
 	void dispatchIncomingConnection(TransportId t, StreamTransportConnection s);
 
-	void dispatchOutgoingConnection(TransportId t, ContactId c,
+	void dispatchOutgoingConnection(TransportIndex i, ContactId c,
 			StreamTransportConnection s);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionReaderFactory.java b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
index fdb7ef91d88bdc1b724e4a1565c27957ff88cf9e..5d7c9142c76a2727a146783a03f127c38c228866 100644
--- a/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
+++ b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
@@ -2,7 +2,7 @@ package net.sf.briar.api.transport;
 
 import java.io.InputStream;
 
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 
 public interface ConnectionReaderFactory {
 
@@ -10,13 +10,13 @@ public interface ConnectionReaderFactory {
 	 * Creates a connection reader for a batch-mode connection or the
 	 * initiator's side of a stream-mode connection.
 	 */
-	ConnectionReader createConnectionReader(InputStream in, TransportId t,
+	ConnectionReader createConnectionReader(InputStream in, TransportIndex i,
 			byte[] encryptedIv, byte[] secret);
 
 	/**
 	 * Creates a connection reader for the responder's side of a stream-mode
 	 * connection.
 	 */
-	ConnectionReader createConnectionReader(InputStream in, TransportId t,
+	ConnectionReader createConnectionReader(InputStream in, TransportIndex i,
 			long connection, byte[] secret);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionRecogniser.java b/api/net/sf/briar/api/transport/ConnectionRecogniser.java
index b57af58d4beda85ddfb54fc41058e5a32f4484e5..7336f002dfc9ec8696c71bdc2f66cc4e03cb3694 100644
--- a/api/net/sf/briar/api/transport/ConnectionRecogniser.java
+++ b/api/net/sf/briar/api/transport/ConnectionRecogniser.java
@@ -1,18 +1,16 @@
 package net.sf.briar.api.transport;
 
-import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.DbException;
 
 /**
- * Maintains a transport plugin's connection reordering window and decides
- * whether incoming connections should be accepted or rejected.
+ * Maintains the connection reordering windows and decides whether incoming
+ * connections should be accepted or rejected.
  */
 public interface ConnectionRecogniser {
 
 	/**
-	 * Returns the ID of the contact who created the encrypted IV if the
-	 * connection should be accepted, or null if the connection should be
-	 * rejected.
+	 * Returns the connection's context if the connection should be accepted,
+	 * or null if the connection should be rejected.
 	 */
-	ContactId acceptConnection(byte[] encryptedIv) throws DbException;
+	ConnectionContext acceptConnection(byte[] encryptedIv) throws DbException;
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionRecogniserFactory.java b/api/net/sf/briar/api/transport/ConnectionRecogniserFactory.java
deleted file mode 100644
index 688b453dbf9462de8e1edb70ebe7288ed623e03a..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/ConnectionRecogniserFactory.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.sf.briar.api.transport;
-
-import net.sf.briar.api.TransportId;
-
-public interface ConnectionRecogniserFactory {
-
-	ConnectionRecogniser createConnectionRecogniser(TransportId t);
-}
diff --git a/api/net/sf/briar/api/transport/ConnectionWriterFactory.java b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
index 07064512222631314835c51d4e4e96c301ebb64b..63a13a3619f372e3a00a4a19278eb64931beea5a 100644
--- a/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
+++ b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
@@ -2,7 +2,7 @@ package net.sf.briar.api.transport;
 
 import java.io.OutputStream;
 
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 
 public interface ConnectionWriterFactory {
 
@@ -11,12 +11,12 @@ public interface ConnectionWriterFactory {
 	 * initiator's side of a stream-mode connection.
 	 */
 	ConnectionWriter createConnectionWriter(OutputStream out, long capacity,
-			TransportId t, long connection, byte[] secret);
+			TransportIndex i, long connection, byte[] secret);
 
 	/**
 	 * Creates a connection writer for the responder's side of a stream-mode
 	 * connection.
 	 */
 	ConnectionWriter createConnectionWriter(OutputStream out, long capacity,
-			TransportId t, byte[] encryptedIv, byte[] secret);
+			TransportIndex i, byte[] encryptedIv, byte[] secret);
 }
diff --git a/api/net/sf/briar/api/transport/StreamConnectionFactory.java b/api/net/sf/briar/api/transport/StreamConnectionFactory.java
index ee29406c5906e4e5dd012ebbe1f7ca1a10ac4bfa..621a072518b594e40a79e0653e84c4efcded2aef 100644
--- a/api/net/sf/briar/api/transport/StreamConnectionFactory.java
+++ b/api/net/sf/briar/api/transport/StreamConnectionFactory.java
@@ -1,13 +1,13 @@
 package net.sf.briar.api.transport;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 
 public interface StreamConnectionFactory {
 
-	void createIncomingConnection(TransportId t, ContactId c, 
+	void createIncomingConnection(TransportIndex i, ContactId c, 
 			StreamTransportConnection s, byte[] encryptedIv);
 
-	void createOutgoingConnection(TransportId t, ContactId c,
+	void createOutgoingConnection(TransportIndex i, ContactId c,
 			StreamTransportConnection s);
 }
diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index e8c9d05bd8e5874e260a6c40721dc4c5c5f8ddf3..39eb9889af59403a7dee2bec971e57215acfa546 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -7,7 +7,6 @@ import java.util.Map;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
@@ -18,6 +17,9 @@ import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWindow;
 
 /**
@@ -78,14 +80,12 @@ interface Database<T> {
 	void addBatchToAck(T txn, ContactId c, BatchId b) throws DbException;
 
 	/**
-	 * Adds a new contact to the database with the given transport properties
-	 * and secret, and returns an ID for the contact.
+	 * Adds a new contact to the database with the given secret and returns an
+	 * ID for the contact.
 	 * <p>
-	 * Locking: contact write, transport write.
+	 * Locking: contact write.
 	 */
-	ContactId addContact(T txn,
-			Map<TransportId, TransportProperties> transports, byte[] secret)
-	throws DbException;
+	ContactId addContact(T txn, byte[] secret) throws DbException;
 
 	/**
 	 * Returns false if the given message is already in the database. Otherwise
@@ -118,6 +118,14 @@ interface Database<T> {
 	 */
 	void addSubscription(T txn, Group g) throws DbException;
 
+	/**
+	 * Allocates and returns a local index for the given transport. Returns
+	 * null if all indices have been allocated.
+	 * <p>
+	 * Locking: transport write.
+	 */
+	TransportIndex addTransport(T txn, TransportId t) throws DbException;
+
 	/**
 	 * Returns true if the database contains the given contact.
 	 * <p>
@@ -179,7 +187,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: contact read, window write.
 	 */
-	long getConnectionNumber(T txn, ContactId c, TransportId t)
+	long getConnectionNumber(T txn, ContactId c, TransportIndex i)
 	throws DbException;
 
 	/**
@@ -188,7 +196,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: contact read, window read.
 	 */
-	ConnectionWindow getConnectionWindow(T txn, ContactId c, TransportId t)
+	ConnectionWindow getConnectionWindow(T txn, ContactId c, TransportIndex i)
 	throws DbException;
 
 	/**
@@ -216,6 +224,14 @@ interface Database<T> {
 	 */
 	MessageId getGroupMessageParent(T txn, MessageId m) throws DbException;
 
+	/**
+	 * Returns the local index for the given transport, or null if no index
+	 * has been allocated.
+	 * <p>
+	 * Locking: transport read.
+	 */
+	TransportIndex getLocalIndex(T txn, TransportId t) throws DbException;
+
 	/**
 	 * Returns the local transport properties for the given transport.
 	 * <p>
@@ -225,12 +241,11 @@ interface Database<T> {
 	throws DbException;
 
 	/**
-	 * Returns all local transport properties.
+	 * Returns all local transports.
 	 * <p>
 	 * Locking: transport read.
 	 */
-	Map<TransportId, TransportProperties> getLocalTransports(T txn)
-	throws DbException;
+	Collection<Transport> getLocalTransports(T txn) throws DbException;
 
 	/**
 	 * Returns the IDs of any batches sent to the given contact that should now
@@ -312,6 +327,15 @@ interface Database<T> {
 	 */
 	boolean getRead(T txn, MessageId m) throws DbException;
 
+	/**
+	 * Returns the given contact's index for the given transport, or null if
+	 * the contact does not support the transport.
+	 * <p>
+	 * Locking: contact read, window read.
+	 */
+	TransportIndex getRemoteIndex(T txn, ContactId c, TransportId t)
+	throws DbException;
+
 	/**
 	 * Returns all remote properties for the given transport.
 	 * <p>
@@ -456,7 +480,7 @@ interface Database<T> {
 	 * Removes a contact (and all associated state) from the database.
 	 * <p>
 	 * Locking: contact write, message write, messageFlag write,
-	 * messageStatus write, subscription write, transport write.
+	 * messageStatus write, subscription write, transport write, window write.
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
@@ -501,7 +525,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: contact read, window write.
 	 */
-	void setConnectionWindow(T txn, ContactId c, TransportId t,
+	void setConnectionWindow(T txn, ContactId c, TransportIndex i,
 			ConnectionWindow w) throws DbException;
 
 	/**
@@ -591,15 +615,13 @@ interface Database<T> {
 	throws DbException;
 
 	/**
-	 * Sets the transport properties for the given contact, replacing any
-	 * existing properties unless the existing properties have a newer
-	 * timestamp.
+	 * Sets the transports for the given contact, replacing any existing
+	 * transports unless the existing transports have a newer timestamp.
 	 * <p>
 	 * Locking: contact read, transport write.
 	 */
-	void setTransports(T txn, ContactId c,
-			Map<TransportId, TransportProperties> transports, long timestamp)
-	throws DbException;
+	void setTransports(T txn, ContactId c, Collection<Transport> transports,
+			long timestamp) throws DbException;
 
 	/**
 	 * Records the time at which the local transports were last modified.
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 2457bd98bbd6051b72ca447b3cf46d51a0f8c256..23b3ae4e14a89bbfdbca4f26307fe2b460339219 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -23,7 +23,6 @@ import net.sf.briar.api.Bytes;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
@@ -35,10 +34,12 @@ import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
+import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportsUpdatedEvent;
+import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Batch;
@@ -49,6 +50,9 @@ import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
@@ -131,27 +135,19 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public ContactId addContact(
-			Map<TransportId, TransportProperties> transports, byte[] secret)
-	throws DbException {
+	public ContactId addContact(byte[] secret) throws DbException {
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Adding contact");
 		ContactId c;
 		contactLock.writeLock().lock();
 		try {
-			transportLock.writeLock().lock();
+			T txn = db.startTransaction();
 			try {
-				T txn = db.startTransaction();
-				try {
-					c = db.addContact(txn, transports, secret);
-					db.commitTransaction(txn);
-					if(LOG.isLoggable(Level.FINE))
-						LOG.fine("Added contact " + c);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				transportLock.writeLock().unlock();
+				c = db.addContact(txn, secret);
+				db.commitTransaction(txn);
+				if(LOG.isLoggable(Level.FINE)) LOG.fine("Added contact " + c);
+			} catch(DbException e) {
+				db.abortTransaction(txn);
+				throw e;
 			}
 		} finally {
 			contactLock.writeLock().unlock();
@@ -370,6 +366,26 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public TransportIndex addTransport(TransportId t) throws DbException {
+		TransportIndex i;
+		transportLock.writeLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				i = db.addTransport(txn, t);
+				db.commitTransaction(txn);
+			} catch(DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			transportLock.writeLock().unlock();
+		}
+		// Call the listeners outside the lock
+		if(i != null) callListeners(new TransportAddedEvent(t));
+		return i;
+	}
+
 	public boolean generateAck(ContactId c, AckWriter a) throws DbException,
 	IOException {
 		contactLock.readLock().lock();
@@ -630,7 +646,7 @@ DatabaseCleaner.Callback {
 
 	public void generateTransportUpdate(ContactId c, TransportWriter t)
 	throws DbException, IOException {
-		Map<TransportId, TransportProperties> transports = null;
+		Collection<Transport> transports = null;
 		long timestamp = 0L;
 		contactLock.readLock().lock();
 		try {
@@ -683,7 +699,7 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public long getConnectionNumber(ContactId c, TransportId t)
+	public long getConnectionNumber(ContactId c, TransportIndex i)
 	throws DbException {
 		contactLock.readLock().lock();
 		try {
@@ -692,7 +708,7 @@ DatabaseCleaner.Callback {
 			try {
 				T txn = db.startTransaction();
 				try {
-					long outgoing = db.getConnectionNumber(txn, c, t);
+					long outgoing = db.getConnectionNumber(txn, c, i);
 					db.commitTransaction(txn);
 					return outgoing;
 				} catch(DbException e) {
@@ -707,7 +723,7 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public ConnectionWindow getConnectionWindow(ContactId c, TransportId t)
+	public ConnectionWindow getConnectionWindow(ContactId c, TransportIndex i)
 	throws DbException {
 		contactLock.readLock().lock();
 		try {
@@ -716,7 +732,7 @@ DatabaseCleaner.Callback {
 			try {
 				T txn = db.startTransaction();
 				try {
-					ConnectionWindow w = db.getConnectionWindow(txn, c, t);
+					ConnectionWindow w = db.getConnectionWindow(txn, c, i);
 					db.commitTransaction(txn);
 					return w;
 				} catch(DbException e) {
@@ -748,6 +764,23 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public TransportIndex getLocalIndex(TransportId t) throws DbException {
+		transportLock.readLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				TransportIndex i = db.getLocalIndex(txn, t);
+				db.commitTransaction(txn);
+				return i;
+			} catch(DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			transportLock.readLock().unlock();
+		}
+	}
+
 	public TransportProperties getLocalProperties(TransportId t)
 	throws DbException {
 		transportLock.readLock().lock();
@@ -766,14 +799,12 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Map<TransportId, TransportProperties> getLocalTransports()
-	throws DbException {
+	public Collection<Transport> getLocalTransports() throws DbException {
 		transportLock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Map<TransportId, TransportProperties> transports =
-					db.getLocalTransports(txn);
+				Collection<Transport> transports = db.getLocalTransports(txn);
 				db.commitTransaction(txn);
 				return transports;
 			} catch(DbException e) {
@@ -826,6 +857,30 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public TransportIndex getRemoteIndex(ContactId c, TransportId t)
+	throws DbException {
+		contactLock.readLock().lock();
+		try {
+			if(!containsContact(c)) throw new NoSuchContactException();
+			transportLock.readLock().lock();
+			try {
+				T txn = db.startTransaction();
+				try {
+					TransportIndex i = db.getRemoteIndex(txn, c, t);
+					db.commitTransaction(txn);
+					return i;
+				} catch(DbException e) {
+					db.abortTransaction(txn);
+					throw e;
+				}
+			} finally {
+				transportLock.readLock().unlock();
+			}
+		} finally {
+			contactLock.readLock().unlock();
+		}
+	}
+
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			TransportId t) throws DbException {
 		contactLock.readLock().lock();
@@ -1135,6 +1190,8 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
+		// Call the listeners outside the lock
+		callListeners(new SubscriptionsUpdatedEvent(Collections.singleton(c)));
 	}
 
 	public void receiveTransportUpdate(ContactId c, TransportUpdate t)
@@ -1147,8 +1204,7 @@ DatabaseCleaner.Callback {
 			try {
 				T txn = db.startTransaction();
 				try {
-					Map<TransportId, TransportProperties> transports =
-						t.getTransports();
+					Collection<Transport> transports = t.getTransports();
 					db.setTransports(txn, c, transports, t.getTimestamp());
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Received " + transports.size()
@@ -1164,6 +1220,8 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
+		// Call the listeners outside the lock
+		callListeners(new RemoteTransportsUpdatedEvent(c));
 	}
 
 	public void removeContact(ContactId c) throws DbException {
@@ -1180,13 +1238,18 @@ DatabaseCleaner.Callback {
 						try {
 							transportLock.writeLock().lock();
 							try {
-								T txn = db.startTransaction();
+								windowLock.writeLock().lock();
 								try {
-									db.removeContact(txn, c);
-									db.commitTransaction(txn);
-								} catch(DbException e) {
-									db.abortTransaction(txn);
-									throw e;
+									T txn = db.startTransaction();
+									try {
+										db.removeContact(txn, c);
+										db.commitTransaction(txn);
+									} catch(DbException e) {
+										db.abortTransaction(txn);
+										throw e;
+									}
+								} finally {
+									windowLock.writeLock().unlock();
 								}
 							} finally {
 								transportLock.writeLock().unlock();
@@ -1227,7 +1290,7 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void setConnectionWindow(ContactId c, TransportId t,
+	public void setConnectionWindow(ContactId c, TransportIndex i,
 			ConnectionWindow w) throws DbException {
 		contactLock.readLock().lock();
 		try {
@@ -1236,7 +1299,7 @@ DatabaseCleaner.Callback {
 			try {
 				T txn = db.startTransaction();
 				try {
-					db.setConnectionWindow(txn, c, t, w);
+					db.setConnectionWindow(txn, c, i, w);
 					db.commitTransaction(txn);
 				} catch(DbException e) {
 					db.abortTransaction(txn);
@@ -1270,7 +1333,7 @@ DatabaseCleaner.Callback {
 			transportLock.writeLock().unlock();
 		}
 		// Call the listeners outside the lock
-		if(changed) callListeners(new TransportsUpdatedEvent());
+		if(changed) callListeners(new LocalTransportsUpdatedEvent());
 	}
 
 	public void setRating(AuthorId a, Rating r) throws DbException {
@@ -1430,6 +1493,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			subscriptionLock.writeLock().unlock();
 		}
+		// Listeners will be notified when the group's visibility is set
 	}
 
 	public void unsubscribe(GroupId g) throws DbException {
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index 16e691135118dc18378a28f2096064d51cfd60c3..40d519045ff42e6b59e49c2cf53eefe52ade52cc 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -20,7 +20,6 @@ import java.util.logging.Logger;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
@@ -32,6 +31,10 @@ import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.ProtocolConstants;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWindow;
 import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.util.FileUtils;
@@ -169,38 +172,56 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String INDEX_STATUSES_BY_CONTACT =
 		"CREATE INDEX statusesByContact ON statuses (contactId)";
 
-	private static final String CREATE_CONTACT_TRANSPORTS =
-		"CREATE TABLE contactTransports"
-		+ " (contactId INT NOT NULL,"
-		+ " transportId INT NOT NULL,"
-		+ " key VARCHAR NOT NULL,"
-		+ " value VARCHAR NOT NULL,"
-		+ " PRIMARY KEY (contactId, transportId, key),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
-
 	private static final String CREATE_TRANSPORTS =
 		"CREATE TABLE transports"
-		+ " (transportId INT NOT NULL,"
+		+ " (transportId HASH NOT NULL,"
+		+ " index COUNTER,"
+		+ " UNIQUE(transportId),"
+		+ " PRIMARY KEY (transportId, index))";
+
+	private static final String CREATE_TRANSPORT_CONFIGS =
+		"CREATE TABLE transportConfigs"
+		+ " (transportId HASH NOT NULL,"
 		+ " key VARCHAR NOT NULL,"
 		+ " value VARCHAR NOT NULL,"
 		+ " PRIMARY KEY (transportId, key))";
 
-	private static final String CREATE_TRANSPORT_CONFIG =
-		"CREATE TABLE transportConfig"
-		+ " (transportId INT NOT NULL,"
+	private static final String CREATE_TRANSPORT_PROPS =
+		"CREATE TABLE transportProperties"
+		+ " (transportId HASH NOT NULL,"
 		+ " key VARCHAR NOT NULL,"
 		+ " value VARCHAR NOT NULL,"
 		+ " PRIMARY KEY (transportId, key))";
 
+	private static final String CREATE_CONTACT_TRANSPORTS =
+		"CREATE TABLE contactTransports"
+		+ " (contactId INT NOT NULL,"
+		+ " transportId HASH NOT NULL,"
+		+ " index INT NOT NULL,"
+		+ " UNIQUE (contactId, transportId),"
+		+ " UNIQUE (contactId, index),"
+		+ " PRIMARY KEY (contactId, transportId, index),"
+		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+		+ " ON DELETE CASCADE)";
+
+	private static final String CREATE_CONTACT_TRANSPORT_PROPS =
+		"CREATE TABLE contactTransportProperties"
+		+ " (contactId INT NOT NULL,"
+		+ " transportId HASH NOT NULL,"
+		+ " key VARCHAR NOT NULL,"
+		+ " value VARCHAR NOT NULL,"
+		+ " PRIMARY KEY (contactId, transportId, key),"
+		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+		+ " ON DELETE CASCADE)";
+
 	private static final String CREATE_CONNECTION_WINDOWS =
 		"CREATE TABLE connectionWindows"
 		+ " (contactId INT NOT NULL,"
-		+ " transportId INT NOT NULL,"
+		+ " index INT NOT NULL,"
 		+ " centre BIGINT NOT NULL,"
 		+ " bitmap INT NOT NULL,"
 		+ " outgoing BIGINT NOT NULL,"
-		+ " PRIMARY KEY (contactId, transportId),"
+		+ " PRIMARY KEY (contactId, index),"
 		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 		+ " ON DELETE CASCADE)";
 
@@ -316,9 +337,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_STATUSES));
 			s.executeUpdate(INDEX_STATUSES_BY_MESSAGE);
 			s.executeUpdate(INDEX_STATUSES_BY_CONTACT);
-			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORTS));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
-			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIG));
+			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIGS));
+			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_PROPS));
+			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORTS));
+			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORT_PROPS));
 			s.executeUpdate(insertTypeNames(CREATE_CONNECTION_WINDOWS));
 			s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMESTAMPS));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS));
@@ -478,8 +501,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public ContactId addContact(Connection txn,
-			Map<TransportId, TransportProperties> transports, byte[] secret)
+	public ContactId addContact(Connection txn, byte[] secret)
 	throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -502,29 +524,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(rs.next()) throw new DbStateException();
 			rs.close();
 			ps.close();
-			// Store the contact's transport properties
-			sql = "INSERT INTO contactTransports"
-				+ " (contactId, transportId, key, value)"
-				+ " VALUES (?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			int batchSize = 0;
-			for(Entry<TransportId, TransportProperties> e
-					: transports.entrySet()) {
-				ps.setInt(2, e.getKey().getInt());
-				for(Entry<String, String> e1 : e.getValue().entrySet()) {
-					ps.setString(3, e1.getKey());
-					ps.setString(4, e1.getValue());
-					ps.addBatch();
-					batchSize++;
-				}
-			}
-			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != batchSize) throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
-			ps.close();
 			// Initialise the subscription timestamps
 			sql = "INSERT INTO subscriptionTimestamps"
 				+ " (contactId, sent, received, modified)"
@@ -693,6 +692,44 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public TransportIndex addTransport(Connection txn, TransportId t)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			// Allocate a new index
+			String sql = "INSERT INTO transports (transportId) VALUES (?)";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			int affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			ps.close();
+			// If the new index is in range, return it
+			sql = "SELECT index FROM transports WHERE transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			int i = rs.getInt(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			if(i < ProtocolConstants.MAX_TRANSPORTS)
+				return new TransportIndex(i);
+			// Too many transports - delete the new index and return null
+			sql = "DELETE FROM transports WHERE transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			return null;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public boolean containsContact(Connection txn, ContactId c)
 	throws DbException {
 		PreparedStatement ps = null;
@@ -836,10 +873,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM transportConfig"
+			String sql = "SELECT key, value FROM transportConfigs"
 				+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
 			TransportConfig c = new TransportConfig();
 			while(rs.next()) c.put(rs.getString(1), rs.getString(2));
@@ -854,15 +891,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public long getConnectionNumber(Connection txn, ContactId c,
-			TransportId t) throws DbException {
+			TransportIndex i) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT outgoing FROM connectionWindows"
-				+ " WHERE contactId = ? AND transportId = ?";
+				+ " WHERE contactId = ? AND index = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, t.getInt());
+			ps.setInt(2, i.getInt());
 			rs = ps.executeQuery();
 			if(rs.next()) {
 				// A connection window row exists - update it
@@ -871,11 +908,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 				rs.close();
 				ps.close();
 				sql = "UPDATE connectionWindows SET outgoing = ?"
-					+ " WHERE contactId = ? AND transportId = ?";
+					+ " WHERE contactId = ? AND index = ?";
 				ps = txn.prepareStatement(sql);
 				ps.setLong(1, outgoing + 1);
 				ps.setInt(2, c.getInt());
-				ps.setInt(3, t.getInt());
+				ps.setInt(3, i.getInt());
 				int affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
@@ -885,11 +922,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 				rs.close();
 				ps.close();
 				sql = "INSERT INTO connectionWindows"
-					+ " (contactId, transportId, centre, bitmap, outgoing)"
+					+ " (contactId, index, centre, bitmap, outgoing)"
 					+ " VALUES(?, ?, ZERO(), ZERO(), ZERO())";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
-				ps.setInt(2, t.getInt());
+				ps.setInt(2, i.getInt());
 				int affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
@@ -903,15 +940,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public ConnectionWindow getConnectionWindow(Connection txn, ContactId c,
-			TransportId t) throws DbException {
+			TransportIndex i) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT centre, bitmap FROM connectionWindows"
-				+ " WHERE contactId = ? AND transportId = ?";
+				+ " WHERE contactId = ? AND index = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, t.getInt());
+			ps.setInt(2, i.getInt());
 			rs = ps.executeQuery();
 			long centre = 0L;
 			int bitmap = 0;
@@ -987,15 +1024,39 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public TransportIndex getLocalIndex(Connection txn, TransportId t)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT index FROM transports WHERE transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, t.getBytes());
+			rs = ps.executeQuery();
+			TransportIndex index = null;
+			if(rs.next()) {
+				index = new TransportIndex(rs.getInt(1));
+				if(rs.next()) throw new DbStateException();
+			}
+			rs.close();
+			ps.close();
+			return index;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public TransportProperties getLocalProperties(Connection txn, TransportId t)
 	throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM transports"
+			String sql = "SELECT key, value FROM transportProperties"
 				+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
 			TransportProperties p = new TransportProperties();
 			while(rs.next()) p.put(rs.getString(1), rs.getString(2));
@@ -1009,26 +1070,31 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Map<TransportId, TransportProperties> getLocalTransports(
-			Connection txn) throws DbException {
+	public Collection<Transport> getLocalTransports(Connection txn)
+	throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT transportId, key, value FROM transports"
-				+ " ORDER BY transportId";
+			String sql = "SELECT transports.transportId, index, key, value"
+				+ " FROM transports LEFT OUTER JOIN transportProperties"
+				+ " ON transports.transportId"
+				+ " = transportProperties.transportId"
+				+ " ORDER BY transports.transportId";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
-			Map<TransportId, TransportProperties> transports =
-				new HashMap<TransportId, TransportProperties>();
-			TransportProperties p = null;
+			Collection<Transport> transports = new ArrayList<Transport>();
 			TransportId lastId = null;
+			Transport t = null;
 			while(rs.next()) {
-				TransportId id = new TransportId(rs.getInt(1));
+				TransportId id = new TransportId(rs.getBytes(1));
 				if(!id.equals(lastId)) {
-					p = new TransportProperties();
-					transports.put(id, p);
+					t = new Transport(id, new TransportIndex(rs.getInt(2)));
+					transports.add(t);
 				}
-				p.put(rs.getString(2), rs.getString(3));
+				// Key and value may be null due to the left outer join
+				String key = rs.getString(3);
+				String value = rs.getString(4);
+				if(key != null && value != null) t.put(key, value);
 			}
 			rs.close();
 			ps.close();
@@ -1357,21 +1423,48 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public TransportIndex getRemoteIndex(Connection txn, ContactId c,
+			TransportId t) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT index FROM contactTransports"
+				+ " WHERE contactId = ? AND transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, t.getBytes());
+			rs = ps.executeQuery();
+			TransportIndex index = null;
+			if(rs.next()) {
+				index = new TransportIndex(rs.getInt(1));
+				if(rs.next()) throw new DbStateException();
+			}
+			rs.close();
+			ps.close();
+			return index;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			Connection txn, TransportId t) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT contactId, key, value FROM contactTransports"
+			String sql = "SELECT contactId, key, value"
+				+ " FROM contactTransportProperties"
 				+ " WHERE transportId = ?"
 				+ " ORDER BY contactId";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
 			Map<ContactId, TransportProperties> properties =
 				new HashMap<ContactId, TransportProperties>();
-			TransportProperties p = null;
 			ContactId lastId = null;
+			TransportProperties p = null;
 			while(rs.next()) {
 				ContactId id = new ContactId(rs.getInt(1));
 				if(!id.equals(lastId)) {
@@ -2034,16 +2127,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			// Delete any existing config for the given transport
-			String sql = "DELETE FROM transportConfig WHERE transportId = ?";
+			String sql = "DELETE FROM transportConfigs WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			ps.executeUpdate();
 			ps.close();
 			// Store the new config
-			sql = "INSERT INTO transportConfig (transportId, key, value)"
+			sql = "INSERT INTO transportConfigs (transportId, key, value)"
 				+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			for(Entry<String, String> e : c.entrySet()) {
 				ps.setString(2, e.getKey());
 				ps.setString(3, e.getValue());
@@ -2063,15 +2156,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setConnectionWindow(Connection txn, ContactId c,
-			TransportId t, ConnectionWindow w) throws DbException {
+			TransportIndex i, ConnectionWindow w) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT NULL FROM connectionWindows"
-				+ " WHERE contactId = ? AND transportId = ?";
+				+ " WHERE contactId = ? AND index = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, t.getInt());
+			ps.setInt(2, i.getInt());
 			rs = ps.executeQuery();
 			boolean found = rs.next();
 			if(rs.next()) throw new DbStateException();
@@ -2080,23 +2173,23 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(found) {
 				// A connection window row exists - update it
 				sql = "UPDATE connectionWindows SET centre = ?, bitmap = ?"
-					+ " WHERE contactId = ? AND transportId = ?";
+					+ " WHERE contactId = ? AND index = ?";
 				ps = txn.prepareStatement(sql);
 				ps.setLong(1, w.getCentre());
 				ps.setInt(2, w.getBitmap());
 				ps.setInt(3, c.getInt());
-				ps.setInt(4, t.getInt());
+				ps.setInt(4, i.getInt());
 				int affected = ps.executeUpdate();
 				if(affected != 1) throw new DbStateException();
 				ps.close();
 			} else {
 				// No connection window row exists - create one
 				sql = "INSERT INTO connectionWindows"
-					+ " (contactId, transportId, centre, bitmap, outgoing)"
+					+ " (contactId, index, centre, bitmap, outgoing)"
 					+ " VALUES(?, ?, ?, ?, ZERO())";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
-				ps.setInt(2, t.getInt());
+				ps.setInt(2, i.getInt());
 				ps.setLong(3, w.getCentre());
 				ps.setInt(4, w.getBitmap());
 				int affected = ps.executeUpdate();
@@ -2115,16 +2208,17 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			// Delete any existing properties for the given transport
-			String sql = "DELETE FROM transports WHERE transportId = ?";
+			String sql = "DELETE FROM transportProperties"
+				+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			ps.executeUpdate();
 			ps.close();
 			// Store the new properties
-			sql = "INSERT INTO transports (transportId, key, value)"
+			sql = "INSERT INTO transportProperties (transportId, key, value)"
 				+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, t.getInt());
+			ps.setBytes(1, t.getBytes());
 			for(Entry<String, String> e : p.entrySet()) {
 				ps.setString(2, e.getKey());
 				ps.setString(3, e.getValue());
@@ -2504,7 +2598,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setTransports(Connection txn, ContactId c,
-			Map<TransportId, TransportProperties> transports, long timestamp)
+			Collection<Transport> transports, long timestamp)
 	throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2527,24 +2621,45 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setInt(1, c.getInt());
 			ps.executeUpdate();
 			ps.close();
+			// Delete any existing transport properties
+			sql = "DELETE FROM contactTransportProperties WHERE contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.executeUpdate();
+			ps.close();
 			// Store the new transports
 			sql = "INSERT INTO contactTransports"
-				+ " (contactId, transportId, key, value)"
-				+ " VALUES (?, ?, ?, ?)";
+				+ " (contactId, transportId, index) VALUES (?, ?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			for(Transport t : transports) {
+				ps.setBytes(2, t.getId().getBytes());
+				ps.setInt(3, t.getIndex().getInt());
+				ps.addBatch();
+			}
+			int[] batchAffected = ps.executeBatch();
+			if(batchAffected.length != transports.size())
+				throw new DbStateException();
+			for(int i = 0; i < batchAffected.length; i++) {
+				if(batchAffected[i] != 1) throw new DbStateException();
+			}
+			ps.close();
+			// Store the new transport properties
+			sql = "INSERT INTO contactTransportProperties"
+				+ " (contactId, transportId, key, value) VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			int batchSize = 0;
-			for(Entry<TransportId, TransportProperties> e
-					: transports.entrySet()) {
-				ps.setInt(2, e.getKey().getInt());
-				for(Entry<String, String> e1 : e.getValue().entrySet()) {
+			for(Transport t : transports) {
+				ps.setBytes(2, t.getId().getBytes());
+				for(Entry<String, String> e1 : t.entrySet()) {
 					ps.setString(3, e1.getKey());
 					ps.setString(4, e1.getValue());
 					ps.addBatch();
 					batchSize++;
 				}
 			}
-			int[] batchAffected = ps.executeBatch();
+			batchAffected = ps.executeBatch();
 			if(batchAffected.length != batchSize) throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] != 1) throw new DbStateException();
diff --git a/components/net/sf/briar/invitation/InvitationWorker.java b/components/net/sf/briar/invitation/InvitationWorker.java
index ddebf3574913cdb75bb7b03252ca2d34e8c0c533..8f2cd77f2befecf092a70e0319946f84dee613b3 100644
--- a/components/net/sf/briar/invitation/InvitationWorker.java
+++ b/components/net/sf/briar/invitation/InvitationWorker.java
@@ -5,15 +5,14 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.invitation.InvitationCallback;
 import net.sf.briar.api.invitation.InvitationParameters;
+import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 import net.sf.briar.util.FileUtils;
@@ -72,7 +71,7 @@ class InvitationWorker implements Runnable {
 		File invitationDat = new File(dir, "invitation.dat");
 		callback.encryptingFile(invitationDat);
 		// FIXME: Create a real invitation
-		Map<TransportId, TransportProperties> transports;
+		Collection<Transport> transports;
 		try {
 			transports = db.getLocalTransports();
 		} catch(DbException e) {
@@ -80,7 +79,7 @@ class InvitationWorker implements Runnable {
 		}
 		FileOutputStream out = new FileOutputStream(invitationDat);
 		Writer w = writerFactory.createWriter(out);
-		w.writeMap(transports);
+		w.writeList(transports);
 		out.flush();
 		out.close();
 		return invitationDat;
diff --git a/components/net/sf/briar/plugins/PluginManagerImpl.java b/components/net/sf/briar/plugins/PluginManagerImpl.java
index beb11d06e6ad177b7e3c7f8159ce399739dfd591..0d65447654c940a9f3329e9519b1d6eab3c6b63c 100644
--- a/components/net/sf/briar/plugins/PluginManagerImpl.java
+++ b/components/net/sf/briar/plugins/PluginManagerImpl.java
@@ -8,12 +8,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
@@ -26,7 +26,9 @@ import net.sf.briar.api.plugins.PluginManager;
 import net.sf.briar.api.plugins.StreamPlugin;
 import net.sf.briar.api.plugins.StreamPluginCallback;
 import net.sf.briar.api.plugins.StreamPluginFactory;
-import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.ProtocolConstants;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.BatchTransportReader;
 import net.sf.briar.api.transport.BatchTransportWriter;
 import net.sf.briar.api.transport.ConnectionDispatcher;
@@ -49,26 +51,32 @@ class PluginManagerImpl implements PluginManager {
 		"net.sf.briar.plugins.socket.SimpleSocketPluginFactory"
 	};
 
-	private final Executor executor;
+	private static final int THREAD_POOL_SIZE = 5;
+
 	private final DatabaseComponent db;
 	private final Poller poller;
 	private final ConnectionDispatcher dispatcher;
 	private final UiCallback uiCallback;
+	private final Executor executor;
 	private final List<BatchPlugin> batchPlugins;
 	private final List<StreamPlugin> streamPlugins;
 
 	@Inject
-	PluginManagerImpl(Executor executor, DatabaseComponent db, Poller poller,
+	PluginManagerImpl(DatabaseComponent db, Poller poller,
 			ConnectionDispatcher dispatcher, UiCallback uiCallback) {
-		this.executor = executor;
 		this.db = db;
 		this.poller = poller;
 		this.dispatcher = dispatcher;
 		this.uiCallback = uiCallback;
+		executor = new ScheduledThreadPoolExecutor(THREAD_POOL_SIZE);
 		batchPlugins = new ArrayList<BatchPlugin>();
 		streamPlugins = new ArrayList<StreamPlugin>();
 	}
 
+	public synchronized int getPluginCount() {
+		return batchPlugins.size() + streamPlugins.size();
+	}
+
 	public synchronized int startPlugins() {
 		Set<TransportId> ids = new HashSet<TransportId>();
 		// Instantiate and start the batch plugins
@@ -81,8 +89,8 @@ class PluginManagerImpl implements PluginManager {
 				BatchPlugin plugin = factory.createPlugin(executor, callback);
 				if(plugin == null) {
 					if(LOG.isLoggable(Level.INFO))
-						LOG.info(factory.getClass().getSimpleName() +
-						" did not create a plugin");
+						LOG.info(factory.getClass().getSimpleName()
+								+ " did not create a plugin");
 					continue;
 				}
 				TransportId id = plugin.getId();
@@ -91,7 +99,14 @@ class PluginManagerImpl implements PluginManager {
 						LOG.warning("Duplicate transport ID: " + id);
 					continue;
 				}
-				callback.setId(id);
+				TransportIndex index = db.getLocalIndex(id);
+				if(index == null) index = db.addTransport(id);
+				if(index == null) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Could not allocate index for ID: " + id);
+					continue;
+				}
+				callback.init(id, index);
 				plugin.start();
 				batchPlugins.add(plugin);
 			} catch(ClassCastException e) {
@@ -122,7 +137,14 @@ class PluginManagerImpl implements PluginManager {
 						LOG.warning("Duplicate transport ID: " + id);
 					continue;
 				}
-				callback.setId(id);
+				TransportIndex index = db.getLocalIndex(id);
+				if(index == null) index = db.addTransport(id);
+				if(index == null) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Could not allocate index for ID: " + id);
+					continue;
+				}
+				callback.init(id, index);
 				plugin.start();
 				streamPlugins.add(plugin);
 			} catch(ClassCastException e) {
@@ -138,7 +160,7 @@ class PluginManagerImpl implements PluginManager {
 		plugins.addAll(batchPlugins);
 		plugins.addAll(streamPlugins);
 		poller.startPolling(plugins);
-		// Return the number of plugins started
+		// Return the number of plugins successfully started
 		return batchPlugins.size() + streamPlugins.size();
 	}
 
@@ -164,17 +186,19 @@ class PluginManagerImpl implements PluginManager {
 			}
 		}
 		streamPlugins.clear();
-		// Return the number of plugins stopped
+		// Return the number of plugins successfully stopped
 		return stopped;
 	}
 
 	private abstract class PluginCallbackImpl implements PluginCallback {
 
 		protected volatile TransportId id = null;
+		protected volatile TransportIndex index = null;
 
-		protected void setId(TransportId id) {
-			assert this.id == null;
+		protected void init(TransportId id, TransportIndex index) {
+			assert this.id == null && this.index == null;
 			this.id = id;
+			this.index = index;
 		}
 
 		public TransportConfig getConfig() {
@@ -219,20 +243,20 @@ class PluginManagerImpl implements PluginManager {
 
 		public void setLocalProperties(TransportProperties p) {
 			assert id != null;
-			if(p.size() > TransportUpdate.MAX_PROPERTIES_PER_PLUGIN) {
+			if(p.size() > ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT) {
 				if(LOG.isLoggable(Level.WARNING))
 					LOG.warning("Plugin " + id + " set too many properties");
 				return;
 			}
 			for(String s : p.keySet()) {
-				if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
+				if(s.length() > ProtocolConstants.MAX_PROPERTY_LENGTH) {
 					if(LOG.isLoggable(Level.WARNING))
 						LOG.warning("Plugin " + id + " set long key: " + s);
 					return;
 				}
 			}
 			for(String s : p.values()) {
-				if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
+				if(s.length() > ProtocolConstants.MAX_PROPERTY_LENGTH) {
 					if(LOG.isLoggable(Level.WARNING))
 						LOG.warning("Plugin " + id + " set long value: " + s);
 					return;
@@ -267,8 +291,8 @@ class PluginManagerImpl implements PluginManager {
 		}
 
 		public void writerCreated(ContactId c, BatchTransportWriter w) {
-			assert id != null;
-			dispatcher.dispatchWriter(id, c, w);
+			assert index != null;
+			dispatcher.dispatchWriter(index, c, w);
 		}
 	}
 
@@ -282,8 +306,8 @@ class PluginManagerImpl implements PluginManager {
 
 		public void outgoingConnectionCreated(ContactId c,
 				StreamTransportConnection s) {
-			assert id != null;
-			dispatcher.dispatchOutgoingConnection(id, c, s);
+			assert index != null;
+			dispatcher.dispatchOutgoingConnection(index, c, s);
 		}
 	}
 }
\ No newline at end of file
diff --git a/components/net/sf/briar/plugins/PollerImpl.java b/components/net/sf/briar/plugins/PollerImpl.java
index 87ee77c0d4fd85937e6c325621aade127c70f94b..708a56baee845e10654f4044cbd700f7573a1acc 100644
--- a/components/net/sf/briar/plugins/PollerImpl.java
+++ b/components/net/sf/briar/plugins/PollerImpl.java
@@ -73,8 +73,6 @@ class PollerImpl implements Poller, Runnable {
 		public int compareTo(PollTime p) {
 			if(time < p.time) return -1;
 			if(time > p.time) return 1;
-			if(plugin.getId().getInt() < p.plugin.getId().getInt()) return -1;
-			if(plugin.getId().getInt() > p.plugin.getId().getInt()) return 1;
 			return 0;
 		}
 	}
diff --git a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
index 75d42231d14a157cc4299d5e688161ea05e3c579..ec0f7a585d5775886d5f36f8a730ce81a514dfb2 100644
--- a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
+++ b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
@@ -18,10 +18,10 @@ import javax.microedition.io.StreamConnection;
 import javax.microedition.io.StreamConnectionNotifier;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.plugins.StreamPlugin;
 import net.sf.briar.api.plugins.StreamPluginCallback;
+import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.transport.StreamTransportConnection;
 import net.sf.briar.plugins.AbstractPlugin;
 import net.sf.briar.util.OsUtils;
@@ -29,7 +29,9 @@ import net.sf.briar.util.StringUtils;
 
 class BluetoothPlugin extends AbstractPlugin implements StreamPlugin {
 
-	public static final int TRANSPORT_ID = 2;
+	public static final byte[] TRANSPORT_ID =
+		StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
+				+ "00a539fd260f08a13a0d8a900cde5e49");
 
 	private static final TransportId id = new TransportId(TRANSPORT_ID);
 	private static final Logger LOG =
diff --git a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
index 64a6763d43ff632e666c09bb5ee321f649bd72fa..af60b0ec87fe3a4ff0f3e98274dbfeb6a3a1c59a 100644
--- a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
+++ b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
@@ -9,13 +9,16 @@ import java.util.concurrent.Executor;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.plugins.BatchPluginCallback;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.util.StringUtils;
 
 class RemovableDrivePlugin extends FilePlugin
 implements RemovableDriveMonitor.Callback {
 
-	public static final int TRANSPORT_ID = 0;
+	public static final byte[] TRANSPORT_ID =
+		StringUtils.fromHexString("7c81bf5c9b1cd557685548c85f976bbd"
+				+ "e633d2418ea2e230e5710fb43c6f8cc0");
 
 	private static final TransportId id = new TransportId(TRANSPORT_ID);
 	private static final Logger LOG =
diff --git a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
index 84a273c56202f1377692e00de96a4819e57abf4b..cfe698b96e8905c7a2c096f0d0aa93c4d54d860b 100644
--- a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
+++ b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
@@ -14,14 +14,17 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.plugins.StreamPluginCallback;
+import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.transport.StreamTransportConnection;
+import net.sf.briar.util.StringUtils;
 
 class SimpleSocketPlugin extends SocketPlugin {
 
-	public static final int TRANSPORT_ID = 1;
+	public static final byte[] TRANSPORT_ID =
+		StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
+				+ "c65a62f87e5a4fc6c284f95908b9007d");
 
 	private static final TransportId id = new TransportId(TRANSPORT_ID);
 	private static final Logger LOG =
diff --git a/components/net/sf/briar/plugins/socket/SocketPlugin.java b/components/net/sf/briar/plugins/socket/SocketPlugin.java
index ae20ba4cca4aa93ae652b4dc26eaa8d089a0c6d9..279a48e06ff5aedcaab6a3c82027ae0dbe894729 100644
--- a/components/net/sf/briar/plugins/socket/SocketPlugin.java
+++ b/components/net/sf/briar/plugins/socket/SocketPlugin.java
@@ -32,8 +32,7 @@ abstract class SocketPlugin extends AbstractPlugin implements StreamPlugin {
 	protected abstract SocketAddress getLocalSocketAddress();
 	protected abstract SocketAddress getRemoteSocketAddress(ContactId c);
 
-	protected SocketPlugin(Executor executor,
-			StreamPluginCallback callback) {
+	protected SocketPlugin(Executor executor, StreamPluginCallback callback) {
 		super(executor);
 		this.callback = callback;
 	}
diff --git a/components/net/sf/briar/protocol/AuthorFactoryImpl.java b/components/net/sf/briar/protocol/AuthorFactoryImpl.java
index f617e5fc89e8a8246be134df0517c3e19ec76203..7356a0414bb239785259e951a2f0ea5c14b532c5 100644
--- a/components/net/sf/briar/protocol/AuthorFactoryImpl.java
+++ b/components/net/sf/briar/protocol/AuthorFactoryImpl.java
@@ -8,6 +8,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
 import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
@@ -28,7 +29,9 @@ class AuthorFactoryImpl implements AuthorFactory {
 	throws IOException {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		new AuthorImpl(null, name, publicKey).writeTo(w);
+		w.writeUserDefinedId(Types.AUTHOR);
+		w.writeString(name);
+		w.writeBytes(publicKey);
 		MessageDigest messageDigest = crypto.getMessageDigest();
 		messageDigest.reset();
 		messageDigest.update(out.toByteArray());
diff --git a/components/net/sf/briar/protocol/AuthorImpl.java b/components/net/sf/briar/protocol/AuthorImpl.java
index f232f932f5dba876c2162e55e096ba56316c50e7..dfaf022c84678e796aa924b60b363ccfe86aca69 100644
--- a/components/net/sf/briar/protocol/AuthorImpl.java
+++ b/components/net/sf/briar/protocol/AuthorImpl.java
@@ -1,11 +1,7 @@
 package net.sf.briar.protocol;
 
-import java.io.IOException;
-
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.serial.Writer;
 
 class AuthorImpl implements Author {
 
@@ -30,10 +26,4 @@ class AuthorImpl implements Author {
 	public byte[] getPublicKey() {
 		return publicKey;
 	}
-
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.AUTHOR);
-		w.writeString(name);
-		w.writeBytes(publicKey);
-	}
 }
diff --git a/components/net/sf/briar/protocol/AuthorReader.java b/components/net/sf/briar/protocol/AuthorReader.java
index 06d509cafaed22598b0d58326eadece18ce20422..4451197bbe0cdc0c43e3757ab0c53bd57f2af6a1 100644
--- a/components/net/sf/briar/protocol/AuthorReader.java
+++ b/components/net/sf/briar/protocol/AuthorReader.java
@@ -7,6 +7,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
 import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
@@ -28,8 +29,8 @@ class AuthorReader implements ObjectReader<Author> {
 		// Read and digest the data
 		r.addConsumer(digesting);
 		r.readUserDefinedId(Types.AUTHOR);
-		String name = r.readString(Author.MAX_NAME_LENGTH);
-		byte[] publicKey = r.readBytes(Author.MAX_PUBLIC_KEY_LENGTH);
+		String name = r.readString(ProtocolConstants.MAX_AUTHOR_NAME_LENGTH);
+		byte[] publicKey = r.readBytes(ProtocolConstants.MAX_PUBLIC_KEY_LENGTH);
 		r.removeConsumer(digesting);
 		// Build and return the author
 		AuthorId id = new AuthorId(messageDigest.digest());
diff --git a/components/net/sf/briar/protocol/GroupFactoryImpl.java b/components/net/sf/briar/protocol/GroupFactoryImpl.java
index a8411c091d26a3fe9c2d6ad8026cf862aeb6dc7a..6df7b18553fef451c776006d8d8d9ebaf20d6926 100644
--- a/components/net/sf/briar/protocol/GroupFactoryImpl.java
+++ b/components/net/sf/briar/protocol/GroupFactoryImpl.java
@@ -8,6 +8,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
@@ -27,7 +28,10 @@ class GroupFactoryImpl implements GroupFactory {
 	public Group createGroup(String name, byte[] publicKey) throws IOException {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		Writer w = writerFactory.createWriter(out);
-		new GroupImpl(null, name, publicKey).writeTo(w);
+		w.writeUserDefinedId(Types.GROUP);
+		w.writeString(name);
+		if(publicKey == null) w.writeNull();
+		else w.writeBytes(publicKey);
 		MessageDigest messageDigest = crypto.getMessageDigest();
 		messageDigest.reset();
 		messageDigest.update(out.toByteArray());
diff --git a/components/net/sf/briar/protocol/GroupIdReader.java b/components/net/sf/briar/protocol/GroupIdReader.java
deleted file mode 100644
index b121fd8cbc950d0e1c69eefa33768de09eac4d2f..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/GroupIdReader.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.IOException;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.ObjectReader;
-import net.sf.briar.api.serial.Reader;
-
-class GroupIdReader implements ObjectReader<GroupId> {
-
-	public GroupId readObject(Reader r) throws IOException {
-		r.readUserDefinedId(Types.GROUP_ID);
-		byte[] b = r.readBytes(UniqueId.LENGTH);
-		if(b.length != UniqueId.LENGTH) throw new FormatException();
-		return new GroupId(b);
-	}
-}
diff --git a/components/net/sf/briar/protocol/GroupImpl.java b/components/net/sf/briar/protocol/GroupImpl.java
index 4f6f613e904d83c1ed46f823a09600bf980ca28d..88f63622d82873303953c8305202a117842a4ad9 100644
--- a/components/net/sf/briar/protocol/GroupImpl.java
+++ b/components/net/sf/briar/protocol/GroupImpl.java
@@ -1,11 +1,7 @@
 package net.sf.briar.protocol;
 
-import java.io.IOException;
-
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.serial.Writer;
 
 class GroupImpl implements Group {
 
@@ -31,13 +27,6 @@ class GroupImpl implements Group {
 		return publicKey;
 	}
 
-	public void writeTo(Writer w) throws IOException {
-		w.writeUserDefinedId(Types.GROUP);
-		w.writeString(name);
-		if(publicKey == null) w.writeNull();
-		else w.writeBytes(publicKey);
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		return o instanceof Group && id.equals(((Group) o).getId());
diff --git a/components/net/sf/briar/protocol/GroupReader.java b/components/net/sf/briar/protocol/GroupReader.java
index 9b371c19da12da6c009d9abd4db4051c5ea1bb3e..9e5dc2d14b2e62e2e6d186c0a25dc25a4dfb63b0 100644
--- a/components/net/sf/briar/protocol/GroupReader.java
+++ b/components/net/sf/briar/protocol/GroupReader.java
@@ -7,6 +7,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
@@ -28,10 +29,10 @@ class GroupReader implements ObjectReader<Group> {
 		// Read and digest the data
 		r.addConsumer(digesting);
 		r.readUserDefinedId(Types.GROUP);
-		String name = r.readString(Group.MAX_NAME_LENGTH);
+		String name = r.readString(ProtocolConstants.MAX_GROUP_NAME_LENGTH);
 		byte[] publicKey = null;
 		if(r.hasNull()) r.readNull();
-		else publicKey = r.readBytes(Group.MAX_PUBLIC_KEY_LENGTH);
+		else publicKey = r.readBytes(ProtocolConstants.MAX_PUBLIC_KEY_LENGTH);
 		r.removeConsumer(digesting);
 		// Build and return the group
 		GroupId id = new GroupId(messageDigest.digest());
diff --git a/components/net/sf/briar/protocol/MessageEncoderImpl.java b/components/net/sf/briar/protocol/MessageEncoderImpl.java
index eeef6c410541a860e426df36a107536215b38b7c..821e53016611497d6749a74ab12e4424bcd6e637 100644
--- a/components/net/sf/briar/protocol/MessageEncoderImpl.java
+++ b/components/net/sf/briar/protocol/MessageEncoderImpl.java
@@ -14,10 +14,12 @@ import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
+import net.sf.briar.api.protocol.writers.AuthorWriter;
+import net.sf.briar.api.protocol.writers.GroupWriter;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.serial.Consumer;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
@@ -30,14 +32,19 @@ class MessageEncoderImpl implements MessageEncoder {
 	private final SecureRandom random;
 	private final MessageDigest messageDigest;
 	private final WriterFactory writerFactory;
+	private final AuthorWriter authorWriter;
+	private final GroupWriter groupWriter;
 
 	@Inject
-	MessageEncoderImpl(CryptoComponent crypto, WriterFactory writerFactory) {
+	MessageEncoderImpl(CryptoComponent crypto, WriterFactory writerFactory,
+			AuthorWriter authorWriter, GroupWriter groupWriter) {
 		authorSignature = crypto.getSignature();
 		groupSignature = crypto.getSignature();
 		random = crypto.getSecureRandom();
 		messageDigest = crypto.getMessageDigest();
 		this.writerFactory = writerFactory;
+		this.authorWriter = authorWriter;
+		this.groupWriter = groupWriter;
 	}
 
 	public Message encodeMessage(MessageId parent, String subject, byte[] body)
@@ -74,9 +81,9 @@ class MessageEncoderImpl implements MessageEncoder {
 		if((group == null || group.getPublicKey() == null) !=
 			(groupKey == null))
 			throw new IllegalArgumentException();
-		if(subject.getBytes("UTF-8").length > Message.MAX_SUBJECT_LENGTH)
+		if(subject.getBytes("UTF-8").length > ProtocolConstants.MAX_SUBJECT_LENGTH)
 			throw new IllegalArgumentException();
-		if(body.length > Message.MAX_BODY_LENGTH)
+		if(body.length > ProtocolConstants.MAX_BODY_LENGTH)
 			throw new IllegalArgumentException();
 
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -102,15 +109,15 @@ class MessageEncoderImpl implements MessageEncoder {
 		// Write the message
 		w.writeUserDefinedId(Types.MESSAGE);
 		if(parent == null) w.writeNull();
-		else parent.writeTo(w);
+		else w.writeBytes(parent.getBytes());
 		if(group == null) w.writeNull();
-		else group.writeTo(w);
+		else groupWriter.writeGroup(w, group);
 		if(author == null) w.writeNull();
-		else author.writeTo(w);
+		else authorWriter.writeAuthor(w, author);
 		w.writeString(subject);
 		long timestamp = System.currentTimeMillis();
 		w.writeInt64(timestamp);
-		byte[] salt = new byte[Message.SALT_LENGTH];
+		byte[] salt = new byte[ProtocolConstants.SALT_LENGTH];
 		random.nextBytes(salt);
 		w.writeBytes(salt);
 		w.writeBytes(body);
@@ -121,7 +128,7 @@ class MessageEncoderImpl implements MessageEncoder {
 		} else {
 			w.removeConsumer(authorConsumer);
 			byte[] sig = authorSignature.sign();
-			if(sig.length > Message.MAX_SIGNATURE_LENGTH)
+			if(sig.length > ProtocolConstants.MAX_SIGNATURE_LENGTH)
 				throw new IllegalArgumentException();
 			w.writeBytes(sig);
 		}
@@ -131,7 +138,7 @@ class MessageEncoderImpl implements MessageEncoder {
 		} else {
 			w.removeConsumer(groupConsumer);
 			byte[] sig = groupSignature.sign();
-			if(sig.length > Message.MAX_SIGNATURE_LENGTH)
+			if(sig.length > ProtocolConstants.MAX_SIGNATURE_LENGTH)
 				throw new IllegalArgumentException();
 			w.writeBytes(sig);
 		}
diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java
index 6e35f76102d6434698402b776d1fc5ce1697f044..eece7bde294b471354b607222732eef661173e95 100644
--- a/components/net/sf/briar/protocol/MessageImpl.java
+++ b/components/net/sf/briar/protocol/MessageImpl.java
@@ -4,6 +4,7 @@ import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.ProtocolConstants;
 
 /** A simple in-memory implementation of a message. */
 class MessageImpl implements Message {
@@ -21,7 +22,7 @@ class MessageImpl implements Message {
 			int bodyStart, int bodyLength) {
 		if(bodyStart + bodyLength > raw.length)
 			throw new IllegalArgumentException();
-		if(bodyLength > Message.MAX_BODY_LENGTH)
+		if(bodyLength > ProtocolConstants.MAX_BODY_LENGTH)
 			throw new IllegalArgumentException();
 		this.id = id;
 		this.parent = parent;
diff --git a/components/net/sf/briar/protocol/MessageReader.java b/components/net/sf/briar/protocol/MessageReader.java
index 1039a42d7b9c581f04880903be59435ee93e1526..54502aff02970f7bf4c0d80c2f8acaa15d41ac95 100644
--- a/components/net/sf/briar/protocol/MessageReader.java
+++ b/components/net/sf/briar/protocol/MessageReader.java
@@ -77,15 +77,15 @@ class MessageReader implements ObjectReader<Message> {
 			r.removeObjectReader(Types.AUTHOR);
 		}
 		// Read the subject
-		String subject = r.readString(Message.MAX_SUBJECT_LENGTH);
+		String subject = r.readString(ProtocolConstants.MAX_SUBJECT_LENGTH);
 		// Read the timestamp
 		long timestamp = r.readInt64();
 		if(timestamp < 0L) throw new FormatException();
 		// Read the salt
-		byte[] salt = r.readBytes(Message.SALT_LENGTH);
-		if(salt.length != Message.SALT_LENGTH) throw new FormatException();
+		byte[] salt = r.readBytes(ProtocolConstants.SALT_LENGTH);
+		if(salt.length != ProtocolConstants.SALT_LENGTH) throw new FormatException();
 		// Read the message body
-		byte[] body = r.readBytes(Message.MAX_BODY_LENGTH);
+		byte[] body = r.readBytes(ProtocolConstants.MAX_BODY_LENGTH);
 		// Record the offset of the body within the message
 		int bodyStart = (int) counting.getCount() - body.length;
 		// Record the length of the data covered by the author's signature
@@ -93,13 +93,13 @@ class MessageReader implements ObjectReader<Message> {
 		// Read the author's signature, if there is one
 		byte[] authorSig = null;
 		if(author == null) r.readNull();
-		else authorSig = r.readBytes(Message.MAX_SIGNATURE_LENGTH);
+		else authorSig = r.readBytes(ProtocolConstants.MAX_SIGNATURE_LENGTH);
 		// Record the length of the data covered by the group's signature
 		int signedByGroup = (int) counting.getCount();
 		// Read the group's signature, if there is one
 		byte[] groupSig = null;
 		if(group == null || group.getPublicKey() == null) r.readNull();
-		else groupSig = r.readBytes(Message.MAX_SIGNATURE_LENGTH);
+		else groupSig = r.readBytes(ProtocolConstants.MAX_SIGNATURE_LENGTH);
 		// That's all, folks
 		r.removeConsumer(counting);
 		r.removeConsumer(copying);
diff --git a/components/net/sf/briar/protocol/ProtocolModule.java b/components/net/sf/briar/protocol/ProtocolModule.java
index 6d36d8efb38cc18522aa7a1f9d5a7f83dcb95c53..aa9c033f2554729565ed5b87971ab4ca843728aa 100644
--- a/components/net/sf/briar/protocol/ProtocolModule.java
+++ b/components/net/sf/briar/protocol/ProtocolModule.java
@@ -9,13 +9,13 @@ import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.serial.ObjectReader;
 
 import com.google.inject.AbstractModule;
@@ -33,8 +33,8 @@ public class ProtocolModule extends AbstractModule {
 		bind(OfferFactory.class).to(OfferFactoryImpl.class);
 		bind(ProtocolReaderFactory.class).to(ProtocolReaderFactoryImpl.class);
 		bind(RequestFactory.class).to(RequestFactoryImpl.class);
-		bind(SubscriptionFactory.class).to(SubscriptionFactoryImpl.class);
-		bind(TransportFactory.class).to(TransportFactoryImpl.class);
+		bind(SubscriptionUpdateFactory.class).to(SubscriptionUpdateFactoryImpl.class);
+		bind(TransportUpdateFactory.class).to(TransportUpdateFactoryImpl.class);
 	}
 
 	@Provides
@@ -94,13 +94,13 @@ public class ProtocolModule extends AbstractModule {
 	@Provides
 	ObjectReader<SubscriptionUpdate> getSubscriptionReader(
 			ObjectReader<Group> groupReader,
-			SubscriptionFactory subscriptionFactory) {
-		return new SubscriptionReader(groupReader, subscriptionFactory);
+			SubscriptionUpdateFactory subscriptionFactory) {
+		return new SubscriptionUpdateReader(groupReader, subscriptionFactory);
 	}
 
 	@Provides
 	ObjectReader<TransportUpdate> getTransportReader(
-			TransportFactory transportFactory) {
-		return new TransportReader(transportFactory);
+			TransportUpdateFactory transportFactory) {
+		return new TransportUpdateReader(transportFactory);
 	}
 }
diff --git a/components/net/sf/briar/protocol/SubscriptionFactory.java b/components/net/sf/briar/protocol/SubscriptionUpdateFactory.java
similarity index 86%
rename from components/net/sf/briar/protocol/SubscriptionFactory.java
rename to components/net/sf/briar/protocol/SubscriptionUpdateFactory.java
index ee598296810252a0a036679c891b55c8a997e562..5d04150ececd0f35a58bc21a2c71c781ff2af32c 100644
--- a/components/net/sf/briar/protocol/SubscriptionFactory.java
+++ b/components/net/sf/briar/protocol/SubscriptionUpdateFactory.java
@@ -5,7 +5,7 @@ import java.util.Map;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 
-interface SubscriptionFactory {
+interface SubscriptionUpdateFactory {
 
 	SubscriptionUpdate createSubscriptions(Map<Group, Long> subs,
 			long timestamp);
diff --git a/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java b/components/net/sf/briar/protocol/SubscriptionUpdateFactoryImpl.java
similarity index 80%
rename from components/net/sf/briar/protocol/SubscriptionFactoryImpl.java
rename to components/net/sf/briar/protocol/SubscriptionUpdateFactoryImpl.java
index 07dfdec95266a292e2eed14082de57ab520c7b93..aeb93b3a768f010ac0682bb4b7b86272df88c7e8 100644
--- a/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java
+++ b/components/net/sf/briar/protocol/SubscriptionUpdateFactoryImpl.java
@@ -5,7 +5,7 @@ import java.util.Map;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 
-class SubscriptionFactoryImpl implements SubscriptionFactory {
+class SubscriptionUpdateFactoryImpl implements SubscriptionUpdateFactory {
 
 	public SubscriptionUpdate createSubscriptions(Map<Group, Long> subs,
 			long timestamp) {
diff --git a/components/net/sf/briar/protocol/SubscriptionReader.java b/components/net/sf/briar/protocol/SubscriptionUpdateReader.java
similarity index 83%
rename from components/net/sf/briar/protocol/SubscriptionReader.java
rename to components/net/sf/briar/protocol/SubscriptionUpdateReader.java
index d8bb47a03d6fe8e44438abdb0e3c5be3ad1b6987..b5f37f01f8b53eeda0879da869bce6a82ddcc8a1 100644
--- a/components/net/sf/briar/protocol/SubscriptionReader.java
+++ b/components/net/sf/briar/protocol/SubscriptionUpdateReader.java
@@ -12,13 +12,13 @@ import net.sf.briar.api.serial.Consumer;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
-class SubscriptionReader implements ObjectReader<SubscriptionUpdate> {
+class SubscriptionUpdateReader implements ObjectReader<SubscriptionUpdate> {
 
 	private final ObjectReader<Group> groupReader;
-	private final SubscriptionFactory subscriptionFactory;
+	private final SubscriptionUpdateFactory subscriptionFactory;
 
-	SubscriptionReader(ObjectReader<Group> groupReader,
-			SubscriptionFactory subscriptionFactory) {
+	SubscriptionUpdateReader(ObjectReader<Group> groupReader,
+			SubscriptionUpdateFactory subscriptionFactory) {
 		this.groupReader = groupReader;
 		this.subscriptionFactory = subscriptionFactory;
 	}
diff --git a/components/net/sf/briar/protocol/TransportFactory.java b/components/net/sf/briar/protocol/TransportFactory.java
deleted file mode 100644
index 7cec4d4e1df3f1016d3489af8be1721e22a370a6..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/TransportFactory.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Map;
-
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
-import net.sf.briar.api.protocol.TransportUpdate;
-
-interface TransportFactory {
-
-	TransportUpdate createTransportUpdate(
-			Map<TransportId, TransportProperties> transports, long timestamp);
-}
diff --git a/components/net/sf/briar/protocol/TransportFactoryImpl.java b/components/net/sf/briar/protocol/TransportFactoryImpl.java
deleted file mode 100644
index 25c88070c2467c1b03be1fed42e40e151f1bf063..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/TransportFactoryImpl.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Map;
-
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
-import net.sf.briar.api.protocol.TransportUpdate;
-
-class TransportFactoryImpl implements TransportFactory {
-
-	public TransportUpdate createTransportUpdate(
-			Map<TransportId, TransportProperties> transports, long timestamp) {
-		return new TransportUpdateImpl(transports, timestamp);
-	}
-}
diff --git a/components/net/sf/briar/protocol/TransportReader.java b/components/net/sf/briar/protocol/TransportReader.java
deleted file mode 100644
index 0010d4262fc67a548ed45fc8386aa33d3c70b082..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/TransportReader.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
-import net.sf.briar.api.protocol.ProtocolConstants;
-import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.ObjectReader;
-import net.sf.briar.api.serial.Reader;
-
-class TransportReader implements ObjectReader<TransportUpdate> {
-
-	private final TransportFactory transportFactory;
-	private final ObjectReader<Transport> propertiesReader;
-
-	TransportReader(TransportFactory transportFactory) {
-		this.transportFactory = transportFactory;
-		propertiesReader = new PropertiesReader();
-	}
-
-	public TransportUpdate readObject(Reader r) throws IOException {
-		// Initialise the consumer
-		Consumer counting =
-			new CountingConsumer(ProtocolConstants.MAX_PACKET_LENGTH);
-		// Read the data
-		r.addConsumer(counting);
-		r.readUserDefinedId(Types.TRANSPORT_UPDATE);
-		r.addObjectReader(Types.TRANSPORT_PROPERTIES, propertiesReader);
-		r.setMaxStringLength(ProtocolConstants.MAX_PACKET_LENGTH);
-		List<Transport> l = r.readList(Transport.class);
-		r.resetMaxStringLength();
-		r.removeObjectReader(Types.TRANSPORT_PROPERTIES);
-		if(l.size() > TransportUpdate.MAX_PLUGINS_PER_UPDATE)
-			throw new FormatException();
-		Map<TransportId, TransportProperties> transports =
-			new HashMap<TransportId, TransportProperties>();
-		for(Transport t : l) {
-			if(transports.put(t.id, t.properties) != null)
-				throw new FormatException(); // Duplicate transport ID
-		}
-		long timestamp = r.readInt64();
-		r.removeConsumer(counting);
-		// Build and return the transport update
-		return transportFactory.createTransportUpdate(transports, timestamp);
-	}
-
-	private static class Transport {
-
-		private final TransportId id;
-		private final TransportProperties properties;
-
-		Transport(TransportId id, TransportProperties properties) {
-			this.id = id;
-			this.properties = properties;
-		}
-	}
-
-	private static class PropertiesReader implements ObjectReader<Transport> {
-
-		public Transport readObject(Reader r) throws IOException {
-			r.readUserDefinedId(Types.TRANSPORT_PROPERTIES);
-			int i = r.readInt32();
-			if(i < TransportId.MIN_ID || i > TransportId.MAX_ID)
-				throw new FormatException();
-			TransportId id = new TransportId(i);
-			r.setMaxStringLength(TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
-			Map<String, String> m = r.readMap(String.class, String.class);
-			r.resetMaxStringLength();
-			if(m.size() > TransportUpdate.MAX_PROPERTIES_PER_PLUGIN)
-				throw new FormatException();
-			return new Transport(id, new TransportProperties(m));
-		}
-	}
-}
diff --git a/components/net/sf/briar/protocol/TransportUpdateFactory.java b/components/net/sf/briar/protocol/TransportUpdateFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..0be7251d21d8b88a697677c5447e562da6102be9
--- /dev/null
+++ b/components/net/sf/briar/protocol/TransportUpdateFactory.java
@@ -0,0 +1,12 @@
+package net.sf.briar.protocol;
+
+import java.util.Collection;
+
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportUpdate;
+
+interface TransportUpdateFactory {
+
+	TransportUpdate createTransportUpdate(Collection<Transport> transports,
+			long timestamp);
+}
diff --git a/components/net/sf/briar/protocol/TransportUpdateFactoryImpl.java b/components/net/sf/briar/protocol/TransportUpdateFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf098bcd0bc5a6fb310d7d51f069d385360ccf9c
--- /dev/null
+++ b/components/net/sf/briar/protocol/TransportUpdateFactoryImpl.java
@@ -0,0 +1,14 @@
+package net.sf.briar.protocol;
+
+import java.util.Collection;
+
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportUpdate;
+
+class TransportUpdateFactoryImpl implements TransportUpdateFactory {
+
+	public TransportUpdate createTransportUpdate(
+			Collection<Transport> transports, long timestamp) {
+		return new TransportUpdateImpl(transports, timestamp);
+	}
+}
diff --git a/components/net/sf/briar/protocol/TransportUpdateImpl.java b/components/net/sf/briar/protocol/TransportUpdateImpl.java
index 330275ca0c5c5d3a44ea82920ec4ec6d3649f11c..b69a2bce777c3288e46bbaf67588d2fa48abd842 100644
--- a/components/net/sf/briar/protocol/TransportUpdateImpl.java
+++ b/components/net/sf/briar/protocol/TransportUpdateImpl.java
@@ -1,23 +1,22 @@
 package net.sf.briar.protocol;
 
-import java.util.Map;
+import java.util.Collection;
 
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportUpdate;
 
 class TransportUpdateImpl implements TransportUpdate {
 
-	private final Map<TransportId, TransportProperties> transports;
+	private final Collection<Transport> transports;
 	private final long timestamp;
 
-	TransportUpdateImpl(Map<TransportId, TransportProperties> transports,
+	TransportUpdateImpl(Collection<Transport> transports,
 			long timestamp) {
 		this.transports = transports;
 		this.timestamp = timestamp;
 	}
 
-	public Map<TransportId, TransportProperties> getTransports() {
+	public Collection<Transport> getTransports() {
 		return transports;
 	}
 
diff --git a/components/net/sf/briar/protocol/TransportUpdateReader.java b/components/net/sf/briar/protocol/TransportUpdateReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..5eae4d6138f10b8691fa7f0bfe0adb26e4f1b217
--- /dev/null
+++ b/components/net/sf/briar/protocol/TransportUpdateReader.java
@@ -0,0 +1,79 @@
+package net.sf.briar.protocol;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.protocol.ProtocolConstants;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.Types;
+import net.sf.briar.api.protocol.UniqueId;
+import net.sf.briar.api.serial.Consumer;
+import net.sf.briar.api.serial.ObjectReader;
+import net.sf.briar.api.serial.Reader;
+
+class TransportUpdateReader implements ObjectReader<TransportUpdate> {
+
+	private final TransportUpdateFactory transportUpdateFactory;
+	private final ObjectReader<Transport> transportReader;
+
+	TransportUpdateReader(TransportUpdateFactory transportFactory) {
+		this.transportUpdateFactory = transportFactory;
+		transportReader = new TransportReader();
+	}
+
+	public TransportUpdate readObject(Reader r) throws IOException {
+		// Initialise the consumer
+		Consumer counting =
+			new CountingConsumer(ProtocolConstants.MAX_PACKET_LENGTH);
+		// Read the data
+		r.addConsumer(counting);
+		r.readUserDefinedId(Types.TRANSPORT_UPDATE);
+		r.addObjectReader(Types.TRANSPORT, transportReader);
+		Collection<Transport> transports = r.readList(Transport.class);
+		r.removeObjectReader(Types.TRANSPORT);
+		if(transports.size() > ProtocolConstants.MAX_TRANSPORTS)
+			throw new FormatException();
+		long timestamp = r.readInt64();
+		r.removeConsumer(counting);
+		// Check for duplicate IDs or indices
+		Set<TransportId> ids = new HashSet<TransportId>();
+		Set<TransportIndex> indices = new HashSet<TransportIndex>();
+		for(Transport t : transports) {
+			if(!ids.add(t.getId())) throw new FormatException();
+			if(!indices.add(t.getIndex())) throw new FormatException();
+		}
+		// Build and return the transport update
+		return transportUpdateFactory.createTransportUpdate(transports,
+				timestamp);
+	}
+
+	private class TransportReader implements ObjectReader<Transport> {
+
+		public Transport readObject(Reader r) throws IOException {
+			r.readUserDefinedId(Types.TRANSPORT);
+			// Read the ID
+			byte[] b = r.readBytes(UniqueId.LENGTH);
+			if(b.length != UniqueId.LENGTH) throw new FormatException();
+			TransportId id = new TransportId(b);
+			// Read the index
+			int i = r.readInt32();
+			if(i < 0 || i >= ProtocolConstants.MAX_TRANSPORTS)
+				throw new FormatException();
+			TransportIndex index = new TransportIndex(i);
+			// Read the properties
+			r.setMaxStringLength(ProtocolConstants.MAX_PROPERTY_LENGTH);
+			Map<String, String> m = r.readMap(String.class, String.class);
+			r.resetMaxStringLength();
+			if(m.size() > ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT)
+				throw new FormatException();
+			return new Transport(id, index, m);
+		}
+	}
+}
diff --git a/components/net/sf/briar/protocol/writers/AckWriterImpl.java b/components/net/sf/briar/protocol/writers/AckWriterImpl.java
index ade9ef6bf02d1463e75bfe1af04642d587011dcf..f943b77271c14a00ed51565c1d48d22d9e0af14d 100644
--- a/components/net/sf/briar/protocol/writers/AckWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/AckWriterImpl.java
@@ -41,7 +41,8 @@ class AckWriterImpl implements AckWriter {
 		int overhead = started ? footerLength : headerLength + footerLength;
 		if(capacity < idLength + overhead) return false;
 		if(!started) start();
-		b.writeTo(w);
+		w.writeUserDefinedId(Types.BATCH_ID);
+		w.writeBytes(b.getBytes());
 		capacity -= idLength;
 		return true;
 	}
diff --git a/components/net/sf/briar/protocol/writers/AuthorWriterImpl.java b/components/net/sf/briar/protocol/writers/AuthorWriterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb1d16e90aee71a46f53db552cdcc3b4cc866410
--- /dev/null
+++ b/components/net/sf/briar/protocol/writers/AuthorWriterImpl.java
@@ -0,0 +1,17 @@
+package net.sf.briar.protocol.writers;
+
+import java.io.IOException;
+
+import net.sf.briar.api.protocol.Author;
+import net.sf.briar.api.protocol.Types;
+import net.sf.briar.api.protocol.writers.AuthorWriter;
+import net.sf.briar.api.serial.Writer;
+
+class AuthorWriterImpl implements AuthorWriter {
+
+	public void writeAuthor(Writer w, Author a) throws IOException {
+		w.writeUserDefinedId(Types.AUTHOR);
+		w.writeString(a.getName());
+		w.writeBytes(a.getPublicKey());
+	}
+}
diff --git a/components/net/sf/briar/protocol/writers/GroupWriterImpl.java b/components/net/sf/briar/protocol/writers/GroupWriterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..44974a1f6044f121047582aefe23e55f94d5e854
--- /dev/null
+++ b/components/net/sf/briar/protocol/writers/GroupWriterImpl.java
@@ -0,0 +1,19 @@
+package net.sf.briar.protocol.writers;
+
+import java.io.IOException;
+
+import net.sf.briar.api.protocol.Group;
+import net.sf.briar.api.protocol.Types;
+import net.sf.briar.api.protocol.writers.GroupWriter;
+import net.sf.briar.api.serial.Writer;
+
+class GroupWriterImpl implements GroupWriter {
+
+	public void writeGroup(Writer w, Group g) throws IOException {
+		w.writeUserDefinedId(Types.GROUP);
+		w.writeString(g.getName());
+		byte[] publicKey = g.getPublicKey();
+		if(publicKey == null) w.writeNull();
+		else w.writeBytes(publicKey);
+	}
+}
diff --git a/components/net/sf/briar/protocol/writers/OfferWriterImpl.java b/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
index 0aa54a6d0f8a9b0b6c25dd6c7f8ab5fba3a46b9b..523a534af92e983a239e3f303b25706e929ff45c 100644
--- a/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
@@ -41,7 +41,8 @@ class OfferWriterImpl implements OfferWriter {
 		int overhead = started ? footerLength : headerLength + footerLength;
 		if(capacity < idLength + overhead) return false;
 		if(!started) start();
-		m.writeTo(w);
+		w.writeUserDefinedId(Types.MESSAGE_ID);
+		w.writeBytes(m.getBytes());
 		capacity -= idLength;
 		return true;
 	}
diff --git a/components/net/sf/briar/protocol/writers/ProtocolWritersModule.java b/components/net/sf/briar/protocol/writers/ProtocolWritersModule.java
index 7e83b36b0511020ffe133de15e4f77347271c20a..1afe708d9b259b0949706dfa78cc861480be314c 100644
--- a/components/net/sf/briar/protocol/writers/ProtocolWritersModule.java
+++ b/components/net/sf/briar/protocol/writers/ProtocolWritersModule.java
@@ -1,5 +1,7 @@
 package net.sf.briar.protocol.writers;
 
+import net.sf.briar.api.protocol.writers.AuthorWriter;
+import net.sf.briar.api.protocol.writers.GroupWriter;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 
 import com.google.inject.AbstractModule;
@@ -8,6 +10,8 @@ public class ProtocolWritersModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
+		bind(AuthorWriter.class).to(AuthorWriterImpl.class);
+		bind(GroupWriter.class).to(GroupWriterImpl.class);
 		bind(ProtocolWriterFactory.class).to(ProtocolWriterFactoryImpl.class);
 	}
 }
diff --git a/components/net/sf/briar/protocol/writers/SubscriptionWriterImpl.java b/components/net/sf/briar/protocol/writers/SubscriptionWriterImpl.java
index 82440ba4c312fb1ee54fe3a78981b4b2bed6b354..cf1d8ff60c50b109d4c9d2c217c9229529fef83d 100644
--- a/components/net/sf/briar/protocol/writers/SubscriptionWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/SubscriptionWriterImpl.java
@@ -3,9 +3,11 @@ package net.sf.briar.protocol.writers;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Map;
+import java.util.Map.Entry;
 
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.Types;
+import net.sf.briar.api.protocol.writers.GroupWriter;
 import net.sf.briar.api.protocol.writers.SubscriptionWriter;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
@@ -14,16 +16,23 @@ class SubscriptionWriterImpl implements SubscriptionWriter {
 
 	private final OutputStream out;
 	private final Writer w;
+	private final GroupWriter groupWriter;
 
 	SubscriptionWriterImpl(OutputStream out, WriterFactory writerFactory) {
 		this.out = out;
 		w = writerFactory.createWriter(out);
+		groupWriter = new GroupWriterImpl();
 	}
 
 	public void writeSubscriptions(Map<Group, Long> subs, long timestamp)
 	throws IOException {
 		w.writeUserDefinedId(Types.SUBSCRIPTION_UPDATE);
-		w.writeMap(subs);
+		w.writeMapStart();
+		for(Entry<Group, Long> e : subs.entrySet()) {
+			groupWriter.writeGroup(w, e.getKey());
+			w.writeInt64(e.getValue());
+		}
+		w.writeMapEnd();
 		w.writeInt64(timestamp);
 		out.flush();
 	}
diff --git a/components/net/sf/briar/protocol/writers/TransportWriterImpl.java b/components/net/sf/briar/protocol/writers/TransportWriterImpl.java
index 6e3ca629b735d23d98ec9d3ba3be7f93c181d566..aa58385e49bc285a7acfcc7f7beb34d118b8f34d 100644
--- a/components/net/sf/briar/protocol/writers/TransportWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/TransportWriterImpl.java
@@ -2,11 +2,9 @@ package net.sf.briar.protocol.writers;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Collection;
 
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.writers.TransportWriter;
 import net.sf.briar.api.serial.Writer;
@@ -22,15 +20,15 @@ class TransportWriterImpl implements TransportWriter {
 		w = writerFactory.createWriter(out);
 	}
 
-	public void writeTransports(
-			Map<TransportId, TransportProperties> transports, long timestamp)
-	throws IOException {
+	public void writeTransports(Collection<Transport> transports,
+			long timestamp) throws IOException {
 		w.writeUserDefinedId(Types.TRANSPORT_UPDATE);
 		w.writeListStart();
-		for(Entry<TransportId, TransportProperties> e : transports.entrySet()) {
-			w.writeUserDefinedId(Types.TRANSPORT_PROPERTIES);
-			w.writeInt32(e.getKey().getInt());
-			w.writeMap(e.getValue());
+		for(Transport p : transports) {
+			w.writeUserDefinedId(Types.TRANSPORT);
+			w.writeBytes(p.getId().getBytes());
+			w.writeInt32(p.getIndex().getInt());
+			w.writeMap(p);
 		}
 		w.writeListEnd();
 		w.writeInt64(timestamp);
diff --git a/components/net/sf/briar/serial/WriterImpl.java b/components/net/sf/briar/serial/WriterImpl.java
index c84d989ceabf7cb4a2155f455396a3227cacd4e1..a212a13075fe3c2f8c80874ee39fd0dd10d10035 100644
--- a/components/net/sf/briar/serial/WriterImpl.java
+++ b/components/net/sf/briar/serial/WriterImpl.java
@@ -10,7 +10,6 @@ import java.util.Map.Entry;
 
 import net.sf.briar.api.Bytes;
 import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.Writable;
 import net.sf.briar.api.serial.Writer;
 
 class WriterImpl implements Writer {
@@ -139,8 +138,7 @@ class WriterImpl implements Writer {
 	}
 
 	private void writeObject(Object o) throws IOException {
-		if(o instanceof Writable) ((Writable) o).writeTo(this);
-		else if(o instanceof Boolean) writeBoolean((Boolean) o);
+		if(o instanceof Boolean) writeBoolean((Boolean) o);
 		else if(o instanceof Byte) writeIntAny((Byte) o);
 		else if(o instanceof Short) writeIntAny((Short) o);
 		else if(o instanceof Integer) writeIntAny((Integer) o);
diff --git a/components/net/sf/briar/transport/ConnectionContextImpl.java b/components/net/sf/briar/transport/ConnectionContextImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..eedbf73fe6063dff6790fe228d30209458878e22
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionContextImpl.java
@@ -0,0 +1,38 @@
+package net.sf.briar.transport;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.transport.ConnectionContext;
+
+class ConnectionContextImpl implements ConnectionContext {
+
+	private final ContactId contactId;
+	private final TransportId transportId;
+	private final TransportIndex transportIndex;
+	private final long connectionNumber;
+
+	ConnectionContextImpl(ContactId contactId, TransportId transportId,
+			TransportIndex transportIndex, long connectionNumber) {
+		this.contactId = contactId;
+		this.transportId = transportId;
+		this.transportIndex = transportIndex;
+		this.connectionNumber = connectionNumber;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public TransportId getTransportId() {
+		return transportId;
+	}
+
+	public TransportIndex getTransportIndex() {
+		return transportIndex;
+	}
+
+	public long getConnectionNumber() {
+		return connectionNumber;
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
index 0cc06f95a73f749424139a475639f2fb1270c98a..ca25c792a6145470ce541d7c3a76c7e8ba160473 100644
--- a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
+++ b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
@@ -2,41 +2,41 @@ package net.sf.briar.transport;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.BatchConnectionFactory;
 import net.sf.briar.api.transport.BatchTransportReader;
 import net.sf.briar.api.transport.BatchTransportWriter;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.transport.ConnectionRecogniser;
-import net.sf.briar.api.transport.ConnectionRecogniserFactory;
 import net.sf.briar.api.transport.StreamConnectionFactory;
 import net.sf.briar.api.transport.StreamTransportConnection;
 import net.sf.briar.api.transport.TransportConstants;
 
+import com.google.inject.Inject;
+
 public class ConnectionDispatcherImpl implements ConnectionDispatcher {
 
 	private static final Logger LOG =
 		Logger.getLogger(ConnectionDispatcherImpl.class.getName());
 
-	private final ConnectionRecogniserFactory recFactory;
+	private final ConnectionRecogniser recogniser;
 	private final BatchConnectionFactory batchConnFactory;
 	private final StreamConnectionFactory streamConnFactory;
-	private final Map<TransportId, ConnectionRecogniser> recognisers;
 
-	ConnectionDispatcherImpl(ConnectionRecogniserFactory recFactory,
+	@Inject
+	ConnectionDispatcherImpl(ConnectionRecogniser recogniser,
 			BatchConnectionFactory batchConnFactory,
 			StreamConnectionFactory streamConnFactory) {
-		this.recFactory = recFactory;
+		this.recogniser = recogniser;
 		this.batchConnFactory = batchConnFactory;
 		this.streamConnFactory = streamConnFactory;
-		recognisers = new HashMap<TransportId, ConnectionRecogniser>();
 	}
 
 	public void dispatchReader(TransportId t, BatchTransportReader r) {
@@ -49,21 +49,27 @@ public class ConnectionDispatcherImpl implements ConnectionDispatcher {
 			r.dispose(false);
 			return;
 		}
-		// Get the contact ID, or null if the IV wasn't expected
-		ContactId c;
+		// Get the connection context, or null if the IV wasn't expected
+		ConnectionContext ctx;
 		try {
-			ConnectionRecogniser rec = getRecogniser(t);
-			c = rec.acceptConnection(encryptedIv);
+			ctx = recogniser.acceptConnection(encryptedIv);
 		} catch(DbException e) {
 			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
 			r.dispose(false);
 			return;
 		}
-		if(c == null) {
+		if(ctx == null) {
 			r.dispose(false);
 			return;
 		}
-		batchConnFactory.createIncomingConnection(t, c, r, encryptedIv);
+		if(!t.equals(ctx.getTransportId())) {
+			if(LOG.isLoggable(Level.WARNING))
+				LOG.warning("Connection has unexpected transport ID");
+			r.dispose(false);
+			return;
+		}
+		batchConnFactory.createIncomingConnection(ctx.getTransportIndex(),
+				ctx.getContactId(), r, encryptedIv);
 	}
 
 	private byte[] readIv(InputStream in) throws IOException {
@@ -77,20 +83,9 @@ public class ConnectionDispatcherImpl implements ConnectionDispatcher {
 		return b;
 	}
 
-	private ConnectionRecogniser getRecogniser(TransportId t) {
-		synchronized(recognisers) {
-			ConnectionRecogniser rec = recognisers.get(t);
-			if(rec == null) {
-				rec = recFactory.createConnectionRecogniser(t);
-				recognisers.put(t, rec);
-			}
-			return rec;
-		}
-	}
-
-	public void dispatchWriter(TransportId t, ContactId c,
+	public void dispatchWriter(TransportIndex i, ContactId c,
 			BatchTransportWriter w) {
-		batchConnFactory.createOutgoingConnection(t, c, w);
+		batchConnFactory.createOutgoingConnection(i, c, w);
 	}
 
 	public void dispatchIncomingConnection(TransportId t,
@@ -104,25 +99,31 @@ public class ConnectionDispatcherImpl implements ConnectionDispatcher {
 			s.dispose(false);
 			return;
 		}
-		// Get the contact ID, or null if the IV wasn't expected
-		ContactId c;
+		// Get the connection context, or null if the IV wasn't expected
+		ConnectionContext ctx;
 		try {
-			ConnectionRecogniser rec = getRecogniser(t);
-			c = rec.acceptConnection(encryptedIv);
+			ctx = recogniser.acceptConnection(encryptedIv);
 		} catch(DbException e) {
 			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
 			s.dispose(false);
 			return;
 		}
-		if(c == null) {
+		if(ctx == null) {
+			s.dispose(false);
+			return;
+		}
+		if(!t.equals(ctx.getTransportId())) {
+			if(LOG.isLoggable(Level.WARNING))
+				LOG.warning("Connection has unexpected transport ID");
 			s.dispose(false);
 			return;
 		}
-		streamConnFactory.createIncomingConnection(t, c, s, encryptedIv);
+		streamConnFactory.createIncomingConnection(ctx.getTransportIndex(),
+				ctx.getContactId(), s, encryptedIv);
 	}
 
-	public void dispatchOutgoingConnection(TransportId t, ContactId c,
+	public void dispatchOutgoingConnection(TransportIndex i, ContactId c,
 			StreamTransportConnection s) {
-		streamConnFactory.createOutgoingConnection(t, c, s);
+		streamConnFactory.createOutgoingConnection(i, c, s);
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
index 98d9104f86fc2fea2b4355eff79b6615c68ce706..588837758dfb3f87304aa75c11779b7c51a5e0fb 100644
--- a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
@@ -9,8 +9,8 @@ import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
 
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 
@@ -26,7 +26,7 @@ class ConnectionReaderFactoryImpl implements ConnectionReaderFactory {
 	}
 
 	public ConnectionReader createConnectionReader(InputStream in,
-			TransportId t, byte[] encryptedIv, byte[] secret) {
+			TransportIndex i, byte[] encryptedIv, byte[] secret) {
 		// Decrypt the IV
 		Cipher ivCipher = crypto.getIvCipher();
 		SecretKey ivKey = crypto.deriveIncomingIvKey(secret);
@@ -42,21 +42,22 @@ class ConnectionReaderFactoryImpl implements ConnectionReaderFactory {
 			throw new IllegalArgumentException(badKey);
 		}
 		// Validate the IV
-		if(!IvEncoder.validateIv(iv, true, t))
+		if(!IvEncoder.validateIv(iv, true, i))
 			throw new IllegalArgumentException();
 		// Copy the connection number
 		long connection = IvEncoder.getConnectionNumber(iv);
-		return createConnectionReader(in, true, t, connection, secret);
+		return createConnectionReader(in, true, i, connection, secret);
 	}
 
 	public ConnectionReader createConnectionReader(InputStream in,
-			TransportId t, long connection, byte[] secret) {
-		return createConnectionReader(in, false, t, connection, secret);
+			TransportIndex i, long connection, byte[] secret) {
+		return createConnectionReader(in, false, i, connection, secret);
 	}
 
 	private ConnectionReader createConnectionReader(InputStream in,
-			boolean initiator, TransportId t, long connection, byte[] secret) {
-		byte[] iv = IvEncoder.encodeIv(initiator, t, connection);
+			boolean initiator, TransportIndex i, long connection,
+			byte[] secret) {
+		byte[] iv = IvEncoder.encodeIv(initiator, i, connection);
 		// Create the decrypter
 		Cipher frameCipher = crypto.getFrameCipher();
 		SecretKey frameKey = crypto.deriveIncomingFrameKey(secret);
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserFactoryImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserFactoryImpl.java
deleted file mode 100644
index ed86ff5f942f5fcf3eef866d46a4dd4dfc29f826..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/ConnectionRecogniserFactoryImpl.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.sf.briar.transport;
-
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.transport.ConnectionRecogniser;
-import net.sf.briar.api.transport.ConnectionRecogniserFactory;
-
-import com.google.inject.Inject;
-
-class ConnectionRecogniserFactoryImpl implements ConnectionRecogniserFactory {
-
-	private final CryptoComponent crypto;
-	private final DatabaseComponent db;
-
-	@Inject
-	ConnectionRecogniserFactoryImpl(CryptoComponent crypto,
-			DatabaseComponent db) {
-		this.crypto = crypto;
-		this.db = db;
-	}
-
-	public ConnectionRecogniser createConnectionRecogniser(TransportId t) {
-		return new ConnectionRecogniserImpl(t, crypto, db);
-	}
-}
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
index e2787b0baf04f575646235ccfeb9484c3763ad65..56f728f619ac631f7e37ca188dbad29034431c0e 100644
--- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
+++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
@@ -3,8 +3,13 @@ package net.sf.briar.transport;
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
 
 import java.security.InvalidKeyException;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
@@ -13,128 +18,178 @@ import javax.crypto.SecretKey;
 
 import net.sf.briar.api.Bytes;
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.NoSuchContactException;
-import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.TransportAddedEvent;
+import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionRecogniser;
 import net.sf.briar.api.transport.ConnectionWindow;
 
+import com.google.inject.Inject;
+
 class ConnectionRecogniserImpl implements ConnectionRecogniser,
 DatabaseListener {
 
-	private final TransportId id;
+	private static final Logger LOG =
+		Logger.getLogger(ConnectionRecogniserImpl.class.getName());
+
 	private final CryptoComponent crypto;
 	private final DatabaseComponent db;
-	private final Map<Bytes, ContactId> ivToContact;
-	private final Map<Bytes, Long> ivToConnectionNumber;
-	private final Map<ContactId, Map<Long, Bytes>> contactToIvs;
-	private final Map<ContactId, Cipher> contactToCipher;
-	private final Map<ContactId, ConnectionWindow> contactToWindow;
+	private final Cipher ivCipher;
+	private final Map<Bytes, ConnectionContext> expected;
+	private final Collection<TransportId> localTransportIds;
+
 	private boolean initialised = false;
 
-	ConnectionRecogniserImpl(TransportId id, CryptoComponent crypto,
-			DatabaseComponent db) {
-		this.id = id;
+	@Inject
+	ConnectionRecogniserImpl(CryptoComponent crypto, DatabaseComponent db) {
 		this.crypto = crypto;
 		this.db = db;
-		// FIXME: There's probably a tidier way of maintaining all this state
-		ivToContact = new HashMap<Bytes, ContactId>();
-		ivToConnectionNumber = new HashMap<Bytes, Long>();
-		contactToIvs = new HashMap<ContactId, Map<Long, Bytes>>();
-		contactToCipher = new HashMap<ContactId, Cipher>();
-		contactToWindow = new HashMap<ContactId, ConnectionWindow>();
+		ivCipher = crypto.getIvCipher();
+		expected = new HashMap<Bytes, ConnectionContext>();
+		localTransportIds = new ArrayList<TransportId>();
 		db.addListener(this);
 	}
 
 	private synchronized void initialise() throws DbException {
+		for(Transport t : db.getLocalTransports()) {
+			localTransportIds.add(t.getId());
+		}
 		for(ContactId c : db.getContacts()) {
 			try {
-				// Initialise and store the contact's IV cipher
-				byte[] secret = db.getSharedSecret(c);
-				SecretKey ivKey = crypto.deriveIncomingIvKey(secret);
-				Cipher cipher = crypto.getIvCipher();
-				try {
-					cipher.init(Cipher.ENCRYPT_MODE, ivKey);
-				} catch(InvalidKeyException badKey) {
-					throw new RuntimeException(badKey);
-				}
-				contactToCipher.put(c, cipher);
-				// Calculate the IVs for the contact's connection window
-				ConnectionWindow w = db.getConnectionWindow(c, id);
-				Map<Long, Bytes> ivs = new HashMap<Long, Bytes>();
-				for(Long unseen : w.getUnseenConnectionNumbers()) {
-					Bytes expectedIv = new Bytes(encryptIv(c, unseen));
-					ivToContact.put(expectedIv, c);
-					ivToConnectionNumber.put(expectedIv, unseen);
-					ivs.put(unseen, expectedIv);
-				}
-				contactToIvs.put(c, ivs);
-				contactToWindow.put(c, w);
+				calculateIvs(c);
 			} catch(NoSuchContactException e) {
-				// The contact was removed after the call to getContacts()
-				continue;
+				// The contact was removed - clean up in eventOccurred()
 			}
 		}
 		initialised = true;
 	}
 
-	private synchronized byte[] encryptIv(ContactId c, long connection) {
-		byte[] iv = IvEncoder.encodeIv(true, id, connection);
-		Cipher cipher = contactToCipher.get(c);
-		assert cipher != null;
+	private synchronized void calculateIvs(ContactId c) throws DbException {
+		SecretKey ivKey = crypto.deriveIncomingIvKey(db.getSharedSecret(c));
+		for(TransportId t : localTransportIds) {
+			TransportIndex i = db.getRemoteIndex(c, t);
+			if(i != null) {
+				ConnectionWindow w = db.getConnectionWindow(c, i);
+				calculateIvs(c, t, i, ivKey, w);
+			}
+		}
+	}
+
+	private synchronized void calculateIvs(ContactId c, TransportId t,
+			TransportIndex i, SecretKey ivKey, ConnectionWindow w)
+	throws DbException {
+		for(Long unseen : w.getUnseenConnectionNumbers()) {
+			Bytes iv = new Bytes(encryptIv(i, unseen, ivKey));
+			expected.put(iv, new ConnectionContextImpl(c, t, i, unseen));
+		}
+	}
+
+	private synchronized byte[] encryptIv(TransportIndex i, long connection,
+			SecretKey ivKey) {
+		byte[] iv = IvEncoder.encodeIv(true, i, connection);
 		try {
-			return cipher.doFinal(iv);
+			ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
+			return ivCipher.doFinal(iv);
 		} catch(BadPaddingException badCipher) {
 			throw new RuntimeException(badCipher);
 		} catch(IllegalBlockSizeException badCipher) {
 			throw new RuntimeException(badCipher);
+		} catch(InvalidKeyException badKey) {
+			throw new RuntimeException(badKey);
 		}
 	}
 
-	public synchronized ContactId acceptConnection(byte[] encryptedIv)
+	public synchronized ConnectionContext acceptConnection(byte[] encryptedIv)
 	throws DbException {
 		if(encryptedIv.length != IV_LENGTH)
 			throw new IllegalArgumentException();
 		if(!initialised) initialise();
-		Bytes b = new Bytes(encryptedIv);
-		ContactId contactId = ivToContact.remove(b);
-		Long connection = ivToConnectionNumber.remove(b);
-		assert (contactId == null) == (connection == null);
-		if(contactId == null) return null;
-		// The IV was expected - update and save the connection window
-		ConnectionWindow w = contactToWindow.get(contactId);
-		assert w != null;
-		w.setSeen(connection);
-		db.setConnectionWindow(contactId, id, w);
-		// Update the set of expected IVs
-		Map<Long, Bytes> oldIvs = contactToIvs.remove(contactId);
-		assert oldIvs != null;
-		assert oldIvs.containsKey(connection);
-		Map<Long, Bytes> newIvs = new HashMap<Long, Bytes>();
-		for(Long unseen : w.getUnseenConnectionNumbers()) {
-			Bytes expectedIv = oldIvs.get(unseen);
-			if(expectedIv == null) {
-				expectedIv = new Bytes(encryptIv(contactId, unseen));
-				ivToContact.put(expectedIv, contactId);
-				ivToConnectionNumber.put(expectedIv, connection);
+		ConnectionContext ctx = expected.remove(new Bytes(encryptedIv));
+		if(ctx == null) return null; // The IV was not expected
+		try {
+			ContactId c = ctx.getContactId();
+			TransportIndex i = ctx.getTransportIndex();
+			// Update the connection window
+			ConnectionWindow w = db.getConnectionWindow(c, i);
+			w.setSeen(ctx.getConnectionNumber());
+			db.setConnectionWindow(c, i, w);
+			// Update the set of expected IVs
+			Iterator<ConnectionContext> it = expected.values().iterator();
+			while(it.hasNext()) {
+				ConnectionContext ctx1 = it.next();
+				ContactId c1 = ctx1.getContactId();
+				TransportIndex i1 = ctx1.getTransportIndex();
+				if(c1.equals(c) && i1.equals(i)) it.remove();
 			}
-			newIvs.put(unseen, expectedIv);
+			SecretKey ivKey = crypto.deriveIncomingIvKey(db.getSharedSecret(c));
+			calculateIvs(c, ctx.getTransportId(), i, ivKey, w);
+		} catch(NoSuchContactException e) {
+			// The contact was removed - clean up when we get the event
 		}
-		contactToIvs.put(contactId, newIvs);
-		return contactId;
+		return ctx;
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
-		// When the set of contacts changes we need to re-initialise everything
-		if(e instanceof ContactAddedEvent || e instanceof ContactRemovedEvent) {
+		if(e instanceof ContactRemovedEvent) {
+			// Remove the expected IVs for the ex-contact
+			removeIvs(((ContactRemovedEvent) e).getContactId());
+		} else if(e instanceof TransportAddedEvent) {
+			// Calculate the expected IVs for the new transport
+			TransportId t = ((TransportAddedEvent) e).getTransportId();
+			synchronized(this) {
+				if(!initialised) return;
+				try {
+					localTransportIds.add(t);
+					calculateIvs(t);
+				} catch(DbException e1) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning(e1.getMessage());
+				}
+			}
+		} else if(e instanceof RemoteTransportsUpdatedEvent) {
+			// Remove and recalculate the expected IVs for the contact
+			ContactId c = ((RemoteTransportsUpdatedEvent) e).getContactId();
 			synchronized(this) {
-				initialised = false;
+				if(!initialised) return;
+				removeIvs(c);
+				try {
+					calculateIvs(c);
+				} catch(DbException e1) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning(e1.getMessage());
+				}
+			}
+		}
+	}
+
+	private synchronized void removeIvs(ContactId c) {
+		if(!initialised) return;
+		Iterator<ConnectionContext> it = expected.values().iterator();
+		while(it.hasNext()) if(it.next().getContactId().equals(c)) it.remove();
+	}
+
+	private synchronized void calculateIvs(TransportId t) throws DbException {
+		for(ContactId c : db.getContacts()) {
+			try {
+				byte[] secret = db.getSharedSecret(c);
+				SecretKey ivKey = crypto.deriveIncomingIvKey(secret);
+				TransportIndex i = db.getRemoteIndex(c, t);
+				if(i != null) {
+					ConnectionWindow w = db.getConnectionWindow(c, i);
+					calculateIvs(c, t, i, ivKey, w);
+				}
+			} catch(NoSuchContactException e) {
+				// The contact was removed - clean up when we get the event
 			}
 		}
 	}
diff --git a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
index e899f7135da19fd2249d169b29b4db399f74f28e..cabf922762fd4d7c66e45aacd949f53465399d4e 100644
--- a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
@@ -9,8 +9,8 @@ import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
 
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 
@@ -26,13 +26,14 @@ class ConnectionWriterFactoryImpl implements ConnectionWriterFactory {
 	}
 
 	public ConnectionWriter createConnectionWriter(OutputStream out,
-			long capacity, TransportId t, long connection, byte[] secret) {
-		return createConnectionWriter(out, capacity, true, t, connection,
+			long capacity, TransportIndex i, long connection, byte[] secret) {
+		return createConnectionWriter(out, capacity, true, i, connection,
 				secret);
 	}
 
 	public ConnectionWriter createConnectionWriter(OutputStream out,
-			long capacity, TransportId t, byte[] encryptedIv, byte[] secret) {
+			long capacity, TransportIndex i, byte[] encryptedIv,
+			byte[] secret) {
 		// Decrypt the IV
 		Cipher ivCipher = crypto.getIvCipher();
 		SecretKey ivKey = crypto.deriveIncomingIvKey(secret);
@@ -48,23 +49,23 @@ class ConnectionWriterFactoryImpl implements ConnectionWriterFactory {
 			throw new RuntimeException(badKey);
 		}
 		// Validate the IV
-		if(!IvEncoder.validateIv(iv, true, t))
+		if(!IvEncoder.validateIv(iv, true, i))
 			throw new IllegalArgumentException();
 		// Copy the connection number
 		long connection = IvEncoder.getConnectionNumber(iv);
-		return createConnectionWriter(out, capacity, false, t, connection,
+		return createConnectionWriter(out, capacity, false, i, connection,
 				secret);
 	}
 
 	private ConnectionWriter createConnectionWriter(OutputStream out,
-			long capacity, boolean initiator, TransportId t, long connection,
+			long capacity, boolean initiator, TransportIndex i, long connection,
 			byte[] secret) {
 		// Create the encrypter
 		Cipher ivCipher = crypto.getIvCipher();
 		Cipher frameCipher = crypto.getFrameCipher();
 		SecretKey ivKey = crypto.deriveOutgoingIvKey(secret);
 		SecretKey frameKey = crypto.deriveOutgoingFrameKey(secret);
-		byte[] iv = IvEncoder.encodeIv(initiator, t, connection);
+		byte[] iv = IvEncoder.encodeIv(initiator, i, connection);
 		ConnectionEncrypter encrypter = new ConnectionEncrypterImpl(out,
 				capacity, iv, ivCipher, frameCipher, ivKey, frameKey);
 		// Create the writer
diff --git a/components/net/sf/briar/transport/IvEncoder.java b/components/net/sf/briar/transport/IvEncoder.java
index f0cae6fede17423e37c76b437b9aa5dc700990db..8bba94ba7c3043aacc2360cbcf215610705a65b3 100644
--- a/components/net/sf/briar/transport/IvEncoder.java
+++ b/components/net/sf/briar/transport/IvEncoder.java
@@ -1,18 +1,18 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.util.ByteUtils;
 
 class IvEncoder {
 
-	static byte[] encodeIv(boolean initiator, TransportId transport,
+	static byte[] encodeIv(boolean initiator, TransportIndex i,
 			long connection) {
 		byte[] iv = new byte[IV_LENGTH];
 		// Bit 31 is the initiator flag
 		if(initiator) iv[3] = 1;
 		// Encode the transport identifier as an unsigned 16-bit integer
-		ByteUtils.writeUint16(transport.getInt(), iv, 4);
+		ByteUtils.writeUint16(i.getInt(), iv, 4);
 		// Encode the connection number as an unsigned 32-bit integer
 		ByteUtils.writeUint32(connection, iv, 6);
 		return iv;
@@ -24,16 +24,16 @@ class IvEncoder {
 		ByteUtils.writeUint32(frame, iv, 10);
 	}
 
-	static boolean validateIv(byte[] iv, boolean initiator, TransportId t) {
+	static boolean validateIv(byte[] iv, boolean initiator, TransportIndex i) {
 		if(iv.length != IV_LENGTH) return false;
 		// Check that the reserved bits are all zero
-		for(int i = 0; i < 2; i++) if(iv[i] != 0) return false;
+		for(int j = 0; j < 2; j++) if(iv[j] != 0) return false;
 		if(iv[3] != 0 && iv[3] != 1) return false;
-		for(int i = 10; i < iv.length; i++) if(iv[i] != 0) return false;
+		for(int j = 10; j < iv.length; j++) if(iv[j] != 0) return false;
 		// Check that the initiator flag matches
 		if(initiator != getInitiatorFlag(iv)) return false;
 		// Check that the transport ID matches
-		if(t.getInt() != getTransportId(iv)) return false;
+		if(i.getInt() != getTransportId(iv)) return false;
 		// The IV is valid
 		return true;
 	}
diff --git a/components/net/sf/briar/transport/MacConsumer.java b/components/net/sf/briar/transport/MacConsumer.java
deleted file mode 100644
index f494cf2a554c1fce2d42ba75241b5cc3d6d58829..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/MacConsumer.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.sf.briar.transport;
-
-import javax.crypto.Mac;
-
-import net.sf.briar.api.serial.Consumer;
-
-/** A consumer that passes its input through a MAC. */
-class MacConsumer implements Consumer {
-
-	private final Mac mac;
-
-	MacConsumer(Mac mac) {
-		this.mac = mac;
-	}
-
-	public void write(byte b) {
-		mac.update(b);
-	}
-
-	public void write(byte[] b, int off, int len) {
-		mac.update(b, off, len);
-	}
-}
diff --git a/components/net/sf/briar/transport/TransportModule.java b/components/net/sf/briar/transport/TransportModule.java
index 5d7b4162cc64d57cf83b5d5b788e97db52fd1dee..867b4e39f1cb167a75a21f5cb04e846d799c704b 100644
--- a/components/net/sf/briar/transport/TransportModule.java
+++ b/components/net/sf/briar/transport/TransportModule.java
@@ -1,7 +1,8 @@
 package net.sf.briar.transport;
 
+import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
-import net.sf.briar.api.transport.ConnectionRecogniserFactory;
+import net.sf.briar.api.transport.ConnectionRecogniser;
 import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 
@@ -11,10 +12,10 @@ public class TransportModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
+		bind(ConnectionDispatcher.class).to(ConnectionDispatcherImpl.class);
 		bind(ConnectionReaderFactory.class).to(
 				ConnectionReaderFactoryImpl.class);
-		bind(ConnectionRecogniserFactory.class).to(
-				ConnectionRecogniserFactoryImpl.class);
+		bind(ConnectionRecogniser.class).to(ConnectionRecogniserImpl.class);
 		bind(ConnectionWindowFactory.class).to(
 				ConnectionWindowFactoryImpl.class);
 		bind(ConnectionWriterFactory.class).to(
diff --git a/components/net/sf/briar/transport/batch/BatchConnectionFactoryImpl.java b/components/net/sf/briar/transport/batch/BatchConnectionFactoryImpl.java
index 79a53887553cc3a3bd95fbed02201ea8625f1814..c3329d8492a025e710a856a9f9882a6373a81d4d 100644
--- a/components/net/sf/briar/transport/batch/BatchConnectionFactoryImpl.java
+++ b/components/net/sf/briar/transport/batch/BatchConnectionFactoryImpl.java
@@ -1,9 +1,9 @@
 package net.sf.briar.transport.batch;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.transport.BatchConnectionFactory;
 import net.sf.briar.api.transport.BatchTransportReader;
@@ -33,10 +33,10 @@ class BatchConnectionFactoryImpl implements BatchConnectionFactory {
 		this.protoWriterFactory = protoWriterFactory;
 	}
 
-	public void createIncomingConnection(TransportId t, ContactId c,
+	public void createIncomingConnection(TransportIndex i, ContactId c,
 			BatchTransportReader r, byte[] encryptedIv) {
 		final IncomingBatchConnection conn = new IncomingBatchConnection(
-				connReaderFactory, db, protoReaderFactory, t, c, r,
+				connReaderFactory, db, protoReaderFactory, i, c, r,
 				encryptedIv);
 		Runnable read = new Runnable() {
 			public void run() {
@@ -46,10 +46,10 @@ class BatchConnectionFactoryImpl implements BatchConnectionFactory {
 		new Thread(read).start();
 	}
 
-	public void createOutgoingConnection(TransportId t, ContactId c,
+	public void createOutgoingConnection(TransportIndex i, ContactId c,
 			BatchTransportWriter w) {
 		final OutgoingBatchConnection conn = new OutgoingBatchConnection(
-				connWriterFactory, db, protoWriterFactory, t, c, w);
+				connWriterFactory, db, protoWriterFactory, i, c, w);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
diff --git a/components/net/sf/briar/transport/batch/IncomingBatchConnection.java b/components/net/sf/briar/transport/batch/IncomingBatchConnection.java
index b48f62547b03a6561238570b66f608e724bc0e5e..b1bfd1966cf2232126b3ffcd8e3bd23bae39d240 100644
--- a/components/net/sf/briar/transport/batch/IncomingBatchConnection.java
+++ b/components/net/sf/briar/transport/batch/IncomingBatchConnection.java
@@ -6,7 +6,6 @@ import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.FormatException;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.protocol.Ack;
@@ -14,6 +13,7 @@ import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.transport.BatchTransportReader;
 import net.sf.briar.api.transport.ConnectionReader;
@@ -27,19 +27,19 @@ class IncomingBatchConnection {
 	private final ConnectionReaderFactory connFactory;
 	private final DatabaseComponent db;
 	private final ProtocolReaderFactory protoFactory;
-	private final TransportId transportId;
+	private final TransportIndex transportIndex;
 	private final ContactId contactId;
 	private final BatchTransportReader reader;
 	private final byte[] encryptedIv;
 
 	IncomingBatchConnection(ConnectionReaderFactory connFactory,
 			DatabaseComponent db, ProtocolReaderFactory protoFactory,
-			TransportId transportId, ContactId contactId,
+			TransportIndex transportIndex, ContactId contactId,
 			BatchTransportReader reader, byte[] encryptedIv) {
 		this.connFactory = connFactory;
 		this.db = db;
 		this.protoFactory = protoFactory;
-		this.transportId = transportId;
+		this.transportIndex = transportIndex;
 		this.contactId = contactId;
 		this.reader = reader;
 		this.encryptedIv = encryptedIv;
@@ -49,7 +49,8 @@ class IncomingBatchConnection {
 		try {
 			byte[] secret = db.getSharedSecret(contactId);
 			ConnectionReader conn = connFactory.createConnectionReader(
-					reader.getInputStream(), transportId, encryptedIv, secret);
+					reader.getInputStream(), transportIndex, encryptedIv,
+					secret);
 			ProtocolReader proto = protoFactory.createProtocolReader(
 					conn.getInputStream());
 			// Read packets until EOF
diff --git a/components/net/sf/briar/transport/batch/OutgoingBatchConnection.java b/components/net/sf/briar/transport/batch/OutgoingBatchConnection.java
index bc2e636ed513c37642982b398b7dfb14d8aeb1f5..59207076c3547c6d4b6acd6a4b9c5f4ffebdee0f 100644
--- a/components/net/sf/briar/transport/batch/OutgoingBatchConnection.java
+++ b/components/net/sf/briar/transport/batch/OutgoingBatchConnection.java
@@ -8,9 +8,9 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
@@ -28,18 +28,18 @@ class OutgoingBatchConnection {
 	private final ConnectionWriterFactory connFactory;
 	private final DatabaseComponent db;
 	private final ProtocolWriterFactory protoFactory;
-	private final TransportId transportId;
+	private final TransportIndex transportIndex;
 	private final ContactId contactId;
 	private final BatchTransportWriter writer;
 
 	OutgoingBatchConnection(ConnectionWriterFactory connFactory,
 			DatabaseComponent db, ProtocolWriterFactory protoFactory,
-			TransportId transportId, ContactId contactId,
+			TransportIndex transportIndex, ContactId contactId,
 			BatchTransportWriter writer) {
 		this.connFactory = connFactory;
 		this.db = db;
 		this.protoFactory = protoFactory;
-		this.transportId = transportId;
+		this.transportIndex = transportIndex;
 		this.contactId = contactId;
 		this.writer = writer;
 	}
@@ -47,10 +47,10 @@ class OutgoingBatchConnection {
 	void write() {
 		try {
 			byte[] secret = db.getSharedSecret(contactId);
-			long connection = db.getConnectionNumber(contactId, transportId);
+			long connection = db.getConnectionNumber(contactId, transportIndex);
 			ConnectionWriter conn = connFactory.createConnectionWriter(
-					writer.getOutputStream(), writer.getCapacity(), transportId,
-					connection, secret);
+					writer.getOutputStream(), writer.getCapacity(),
+					transportIndex, connection, secret);
 			OutputStream out = conn.getOutputStream();
 			// There should be enough space for a packet
 			long capacity = conn.getRemainingCapacity();
diff --git a/components/net/sf/briar/transport/stream/IncomingStreamConnection.java b/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
index 5b63487b423b3f2ba3927e9265c49b791ca6e238..3518b75c92b7775e32c8b915f7ff42ae0074409e 100644
--- a/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
+++ b/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
@@ -3,10 +3,10 @@ package net.sf.briar.transport.stream;
 import java.io.IOException;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
@@ -21,11 +21,12 @@ public class IncomingStreamConnection extends StreamConnection {
 	IncomingStreamConnection(ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory, DatabaseComponent db,
 			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory, TransportId transportId,
-			ContactId contactId, StreamTransportConnection connection,
+			ProtocolWriterFactory protoWriterFactory,
+			TransportIndex transportIndex, ContactId contactId,
+			StreamTransportConnection connection,
 			byte[] encryptedIv) {
 		super(connReaderFactory, connWriterFactory, db, protoReaderFactory,
-				protoWriterFactory, transportId, contactId, connection);
+				protoWriterFactory, transportIndex, contactId, connection);
 		this.encryptedIv = encryptedIv;
 	}
 
@@ -34,7 +35,8 @@ public class IncomingStreamConnection extends StreamConnection {
 	IOException {
 		byte[] secret = db.getSharedSecret(contactId);
 		return connReaderFactory.createConnectionReader(
-				connection.getInputStream(), transportId, encryptedIv, secret);
+				connection.getInputStream(), transportIndex, encryptedIv,
+				secret);
 	}
 
 	@Override
@@ -42,7 +44,7 @@ public class IncomingStreamConnection extends StreamConnection {
 	IOException {
 		byte[] secret = db.getSharedSecret(contactId);
 		return connWriterFactory.createConnectionWriter(
-				connection.getOutputStream(), Long.MAX_VALUE, transportId,
+				connection.getOutputStream(), Long.MAX_VALUE, transportIndex,
 				encryptedIv, secret);
 	}
 }
diff --git a/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java b/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
index 5b86b729b178c4fceb7c14a102a437a9b27223a6..923fcf354f88101ca01a6e5d5243d3c0683c4ba5 100644
--- a/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
+++ b/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
@@ -3,10 +3,10 @@ package net.sf.briar.transport.stream;
 import java.io.IOException;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
@@ -21,22 +21,25 @@ public class OutgoingStreamConnection extends StreamConnection {
 	OutgoingStreamConnection(ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory, DatabaseComponent db,
 			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory, TransportId transportId,
-			ContactId contactId, StreamTransportConnection connection) {
+			ProtocolWriterFactory protoWriterFactory,
+			TransportIndex transportIndex, ContactId contactId,
+			StreamTransportConnection connection) {
 		super(connReaderFactory, connWriterFactory, db, protoReaderFactory,
-				protoWriterFactory, transportId, contactId, connection);
+				protoWriterFactory, transportIndex, contactId, connection);
 	}
 
 	@Override
 	protected ConnectionReader createConnectionReader() throws DbException,
 	IOException {
 		synchronized(this) {
-			if(connectionNum == -1L)
-				connectionNum = db.getConnectionNumber(contactId, transportId);
+			if(connectionNum == -1L) {
+				connectionNum = db.getConnectionNumber(contactId,
+						transportIndex);
+			}
 		}
 		byte[] secret = db.getSharedSecret(contactId);
 		return connReaderFactory.createConnectionReader(
-				connection.getInputStream(), transportId, connectionNum,
+				connection.getInputStream(), transportIndex, connectionNum,
 				secret);
 	}
 
@@ -44,12 +47,14 @@ public class OutgoingStreamConnection extends StreamConnection {
 	protected ConnectionWriter createConnectionWriter() throws DbException,
 	IOException {
 		synchronized(this) {
-			if(connectionNum == -1L)
-				connectionNum = db.getConnectionNumber(contactId, transportId);
+			if(connectionNum == -1L) {
+				connectionNum = db.getConnectionNumber(contactId,
+						transportIndex);
+			}
 		}
 		byte[] secret = db.getSharedSecret(contactId);
 		return connWriterFactory.createConnectionWriter(
-				connection.getOutputStream(), Long.MAX_VALUE, transportId,
+				connection.getOutputStream(), Long.MAX_VALUE, transportIndex,
 				connectionNum, secret);
 	}
 }
diff --git a/components/net/sf/briar/transport/stream/StreamConnection.java b/components/net/sf/briar/transport/stream/StreamConnection.java
index 51c2e1ee133c329c0ac24c6a509c8e08ea533318..126e69c4b4ee000d7bc2205c5e276ae8591287f3 100644
--- a/components/net/sf/briar/transport/stream/StreamConnection.java
+++ b/components/net/sf/briar/transport/stream/StreamConnection.java
@@ -12,16 +12,15 @@ import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.FormatException;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.event.BatchReceivedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportsUpdatedEvent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.MessageId;
@@ -30,6 +29,7 @@ import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
@@ -56,7 +56,7 @@ abstract class StreamConnection implements DatabaseListener {
 	protected final DatabaseComponent db;
 	protected final ProtocolReaderFactory protoReaderFactory;
 	protected final ProtocolWriterFactory protoWriterFactory;
-	protected final TransportId transportId;
+	protected final TransportIndex transportIndex;
 	protected final ContactId contactId;
 	protected final StreamTransportConnection connection;
 
@@ -69,14 +69,15 @@ abstract class StreamConnection implements DatabaseListener {
 	StreamConnection(ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory, DatabaseComponent db,
 			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory, TransportId transportId,
-			ContactId contactId, StreamTransportConnection connection) {
+			ProtocolWriterFactory protoWriterFactory,
+			TransportIndex transportIndex, ContactId contactId,
+			StreamTransportConnection connection) {
 		this.connReaderFactory = connReaderFactory;
 		this.connWriterFactory = connWriterFactory;
 		this.db = db;
 		this.protoReaderFactory = protoReaderFactory;
 		this.protoWriterFactory = protoWriterFactory;
-		this.transportId = transportId;
+		this.transportIndex = transportIndex;
 		this.contactId = contactId;
 		this.connection = connection;
 	}
@@ -108,7 +109,7 @@ abstract class StreamConnection implements DatabaseListener {
 					writerFlags |= Flags.SUBSCRIPTIONS_UPDATED;
 					notifyAll();
 				}
-			} else if(e instanceof TransportsUpdatedEvent) {
+			} else if(e instanceof LocalTransportsUpdatedEvent) {
 				writerFlags |= Flags.TRANSPORTS_UPDATED;
 				notifyAll();
 			}
diff --git a/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java b/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
index 21d300e46c14f5be114eb3d9c1bffcaebc4d2003..c3aecf5c54e4235b3337b3babad147d257a2bc96 100644
--- a/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
+++ b/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
@@ -1,9 +1,9 @@
 package net.sf.briar.transport.stream;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
@@ -32,11 +32,11 @@ public class StreamConnectionFactoryImpl implements StreamConnectionFactory {
 		this.protoWriterFactory = protoWriterFactory;
 	}
 
-	public void createIncomingConnection(TransportId t, ContactId c,
+	public void createIncomingConnection(TransportIndex i, ContactId c,
 			StreamTransportConnection s, byte[] encryptedIv) {
 		final StreamConnection conn = new IncomingStreamConnection(
 				connReaderFactory, connWriterFactory, db, protoReaderFactory,
-				protoWriterFactory, t, c, s, encryptedIv);
+				protoWriterFactory, i, c, s, encryptedIv);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
@@ -51,11 +51,11 @@ public class StreamConnectionFactoryImpl implements StreamConnectionFactory {
 		new Thread(read).start();
 	}
 
-	public void createOutgoingConnection(TransportId t, ContactId c,
+	public void createOutgoingConnection(TransportIndex i, ContactId c,
 			StreamTransportConnection s) {
 		final StreamConnection conn = new OutgoingStreamConnection(
 				connReaderFactory, connWriterFactory, db, protoReaderFactory,
-				protoWriterFactory, t, c, s);
+				protoWriterFactory, i, c, s);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
diff --git a/test/net/sf/briar/ProtocolIntegrationTest.java b/test/net/sf/briar/ProtocolIntegrationTest.java
index 3da633eba2e8ed87cc9f43377cdaa76d489ebfe2..9f47b2af6545392c20adf56bcb1919c90caf7285 100644
--- a/test/net/sf/briar/ProtocolIntegrationTest.java
+++ b/test/net/sf/briar/ProtocolIntegrationTest.java
@@ -15,8 +15,6 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 import junit.framework.TestCase;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
@@ -26,17 +24,20 @@ import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.protocol.writers.OfferWriter;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.writers.RequestWriter;
@@ -52,6 +53,8 @@ import net.sf.briar.protocol.ProtocolModule;
 import net.sf.briar.protocol.writers.ProtocolWritersModule;
 import net.sf.briar.serial.SerialModule;
 import net.sf.briar.transport.TransportModule;
+import net.sf.briar.transport.batch.TransportBatchModule;
+import net.sf.briar.transport.stream.TransportStreamModule;
 
 import org.junit.Test;
 
@@ -69,7 +72,7 @@ public class ProtocolIntegrationTest extends TestCase {
 	private final ProtocolWriterFactory protocolWriterFactory;
 	private final CryptoComponent crypto;
 	private final byte[] aliceSecret, bobSecret;
-	private final TransportId transportId = new TransportId(123);
+	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final long connection = 12345L;
 	private final Author author;
 	private final Group group, group1;
@@ -77,14 +80,15 @@ public class ProtocolIntegrationTest extends TestCase {
 	private final String authorName = "Alice";
 	private final String subject = "Hello";
 	private final String messageBody = "Hello world";
-	private final Map<TransportId, TransportProperties> transports;
+	private final Collection<Transport> transports;
 
 	public ProtocolIntegrationTest() throws Exception {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule(),
 				new DatabaseModule(), new ProtocolModule(),
 				new ProtocolWritersModule(), new SerialModule(),
-				new TestDatabaseModule(), new TransportModule());
+				new TestDatabaseModule(), new TransportBatchModule(),
+				new TransportModule(), new TransportStreamModule());
 		connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class);
 		connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
 		protocolReaderFactory = i.getInstance(ProtocolReaderFactory.class);
@@ -120,9 +124,11 @@ public class ProtocolIntegrationTest extends TestCase {
 		message3 = messageEncoder.encodeMessage(null, group1,
 				groupKeyPair.getPrivate(), author, authorKeyPair.getPrivate(),
 				subject, messageBody.getBytes("UTF-8"));
-		TransportProperties p =
-			new TransportProperties(Collections.singletonMap("bar", "baz"));
-		transports = Collections.singletonMap(transportId, p);
+		// Create some transports
+		TransportId transportId = new TransportId(TestUtils.getRandomId());
+		Transport transport = new Transport(transportId, transportIndex,
+				Collections.singletonMap("bar", "baz"));
+		transports = Collections.singletonList(transport);
 	}
 
 	@Test
@@ -134,7 +140,7 @@ public class ProtocolIntegrationTest extends TestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Use Alice's secret for writing
 		ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
-				Long.MAX_VALUE, transportId, connection, aliceSecret);
+				Long.MAX_VALUE, transportIndex, connection, aliceSecret);
 		OutputStream out1 = w.getOutputStream();
 
 		AckWriter a = protocolWriterFactory.createAckWriter(out1);
@@ -188,7 +194,7 @@ public class ProtocolIntegrationTest extends TestCase {
 		assertEquals(16, offset);
 		// Use Bob's secret for reading
 		ConnectionReader r = connectionReaderFactory.createConnectionReader(in,
-				transportId, encryptedIv, bobSecret);
+				transportIndex, encryptedIv, bobSecret);
 		in = r.getInputStream();
 		ProtocolReader protocolReader =
 			protocolReaderFactory.createProtocolReader(in);
diff --git a/test/net/sf/briar/db/DatabaseComponentImplTest.java b/test/net/sf/briar/db/DatabaseComponentImplTest.java
index a7771e174eb9272ef8bdda1193a7dbc9d2b8138b..0c0e2f45ee15dfa63c4a0f38b86e1ccf32629b29 100644
--- a/test/net/sf/briar/db/DatabaseComponentImplTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentImplTest.java
@@ -48,7 +48,7 @@ public class DatabaseComponentImplTest extends DatabaseComponentTest {
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
 			oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP);
-			will(returnValue(Collections.emptySet()));
+			will(returnValue(Collections.emptyList()));
 			oneOf(database).commitTransaction(txn);
 			// As if by magic, some free space has appeared
 			oneOf(database).getFreeSpace();
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index 8b9332217c5f6644146a383de7a7b8425941b7ed..8cabf9a2c1b1b07c0c4547b2508c89d74c18cd47 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -10,7 +10,6 @@ import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.MessageHeader;
@@ -21,7 +20,7 @@ import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
-import net.sf.briar.api.db.event.TransportsUpdatedEvent;
+import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Batch;
@@ -33,6 +32,9 @@ import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
@@ -61,7 +63,8 @@ public abstract class DatabaseComponentTest extends TestCase {
 	private final Message message, privateMessage;
 	private final Group group;
 	private final TransportId transportId;
-	private final Map<TransportId, TransportProperties> transports;
+	private final TransportIndex localIndex, remoteIndex;
+	private final Collection<Transport> transports;
 	private final Map<ContactId, TransportProperties> remoteProperties;
 	private final byte[] secret;
 
@@ -82,11 +85,15 @@ public abstract class DatabaseComponentTest extends TestCase {
 		privateMessage = new TestMessage(messageId, null, null, null, subject,
 				timestamp, raw);
 		group = new TestGroup(groupId, "The really exciting group", null);
-		transportId = new TransportId(123);
-		TransportProperties p =
-			new TransportProperties(Collections.singletonMap("foo", "bar"));
-		transports = Collections.singletonMap(transportId, p);
-		remoteProperties = Collections.singletonMap(contactId, p);
+		transportId = new TransportId(TestUtils.getRandomId());
+		localIndex = new TransportIndex(0);
+		remoteIndex = new TransportIndex(13);
+		TransportProperties properties = new TransportProperties(
+				Collections.singletonMap("foo", "bar"));
+		remoteProperties = Collections.singletonMap(contactId, properties);
+		Transport transport = new Transport(transportId, localIndex,
+				properties);
+		transports = Collections.singletonList(transport);
 		secret = new byte[123];
 	}
 
@@ -124,17 +131,17 @@ public abstract class DatabaseComponentTest extends TestCase {
 			// setRating(authorId, Rating.GOOD) again
 			oneOf(database).setRating(txn, authorId, Rating.GOOD);
 			will(returnValue(Rating.GOOD));
-			// addContact(transports)
-			oneOf(database).addContact(txn, transports, secret);
+			// addContact()
+			oneOf(database).addContact(txn, secret);
 			will(returnValue(contactId));
 			oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class)));
 			// getContacts()
 			oneOf(database).getContacts(txn);
 			will(returnValue(Collections.singletonList(contactId)));
-			// getConnectionWindow(contactId, 123)
+			// getConnectionWindow(contactId, 13)
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getConnectionWindow(txn, contactId, transportId);
+			oneOf(database).getConnectionWindow(txn, contactId, remoteIndex);
 			will(returnValue(connectionWindow));
 			// getSharedSecret(contactId)
 			oneOf(database).containsContact(txn, contactId);
@@ -170,10 +177,10 @@ public abstract class DatabaseComponentTest extends TestCase {
 			// unsubscribe(groupId) again
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(false));
-			// setConnectionWindow(contactId, 123, connectionWindow)
+			// setConnectionWindow(contactId, 13, connectionWindow)
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).setConnectionWindow(txn, contactId, transportId,
+			oneOf(database).setConnectionWindow(txn, contactId, remoteIndex,
 					connectionWindow);
 			// removeContact(contactId)
 			oneOf(database).removeContact(txn, contactId);
@@ -189,10 +196,10 @@ public abstract class DatabaseComponentTest extends TestCase {
 		assertEquals(Rating.UNRATED, db.getRating(authorId));
 		db.setRating(authorId, Rating.GOOD); // First time - listeners called
 		db.setRating(authorId, Rating.GOOD); // Second time - not called
-		assertEquals(contactId, db.addContact(transports, secret));
+		assertEquals(contactId, db.addContact(secret));
 		assertEquals(Collections.singletonList(contactId), db.getContacts());
 		assertEquals(connectionWindow,
-				db.getConnectionWindow(contactId, transportId));
+				db.getConnectionWindow(contactId, remoteIndex));
 		assertEquals(secret, db.getSharedSecret(contactId));
 		assertEquals(remoteProperties, db.getRemoteProperties(transportId));
 		db.subscribe(group); // First time - listeners called
@@ -201,7 +208,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		assertEquals(Collections.singletonList(groupId), db.getSubscriptions());
 		db.unsubscribe(groupId); // First time - listeners called
 		db.unsubscribe(groupId); // Second time - not called
-		db.setConnectionWindow(contactId, transportId, connectionWindow);
+		db.setConnectionWindow(contactId, remoteIndex, connectionWindow);
 		db.removeContact(contactId);
 		db.removeListener(listener);
 		db.close();
@@ -540,7 +547,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		} catch(NoSuchContactException expected) {}
 
 		try {
-			db.getConnectionWindow(contactId, transportId);
+			db.getConnectionWindow(contactId, remoteIndex);
 			fail();
 		} catch(NoSuchContactException expected) {}
 
@@ -580,7 +587,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 		} catch(NoSuchContactException expected) {}
 
 		try {
-			db.setConnectionWindow(contactId, transportId, null);
+			db.setConnectionWindow(contactId, remoteIndex, null);
 			fail();
 		} catch(NoSuchContactException expected) {}
 
@@ -1379,7 +1386,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 					with(any(long.class)));
 			oneOf(database).commitTransaction(txn);
 			oneOf(listener).eventOccurred(with(any(
-					TransportsUpdatedEvent.class)));
+					TransportAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 904b04714a8bdb426d952c47d75a0bb0e0abb8ca..e8d0248fa5251326b23109442fc774ee2cbed157 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -10,7 +10,6 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -22,7 +21,6 @@ import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
 import net.sf.briar.api.TransportConfig;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.crypto.Password;
 import net.sf.briar.api.db.DbException;
@@ -35,12 +33,18 @@ import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWindow;
 import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.protocol.ProtocolModule;
+import net.sf.briar.protocol.writers.ProtocolWritersModule;
 import net.sf.briar.serial.SerialModule;
 import net.sf.briar.transport.TransportModule;
+import net.sf.briar.transport.batch.TransportBatchModule;
+import net.sf.briar.transport.stream.TransportStreamModule;
 
 import org.apache.commons.io.FileSystemUtils;
 import org.junit.After;
@@ -75,17 +79,20 @@ public class H2DatabaseTest extends TestCase {
 	private final Message message, privateMessage;
 	private final Group group;
 	private final TransportId transportId;
+	private final TransportIndex localIndex, remoteIndex;
 	private final TransportProperties properties;
-	private final Map<TransportId, TransportProperties> transports;
 	private final Map<ContactId, TransportProperties> remoteProperties;
+	private final Collection<Transport> remoteTransports;
 	private final Map<Group, Long> subscriptions;
 	private final byte[] secret;
 
 	public H2DatabaseTest() throws Exception {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule(),
-				new DatabaseModule(), new ProtocolModule(), new SerialModule(),
-				new TransportModule(), new TestDatabaseModule(testDir));
+				new DatabaseModule(), new ProtocolModule(),
+				new ProtocolWritersModule(), new SerialModule(),
+				new TransportBatchModule(), new TransportModule(),
+				new TransportStreamModule(), new TestDatabaseModule(testDir));
 		connectionWindowFactory = i.getInstance(ConnectionWindowFactory.class);
 		groupFactory = i.getInstance(GroupFactory.class);
 		authorId = new AuthorId(TestUtils.getRandomId());
@@ -104,11 +111,15 @@ public class H2DatabaseTest extends TestCase {
 		privateMessage = new TestMessage(privateMessageId, null, null, null,
 				subject, timestamp, raw);
 		group = groupFactory.createGroup(groupId, "Group name", null);
-		transportId = new TransportId(0);
+		transportId = new TransportId(TestUtils.getRandomId());
+		localIndex = new TransportIndex(1);
+		remoteIndex = new TransportIndex(13);
 		properties = new TransportProperties(
 				Collections.singletonMap("foo", "bar"));
-		transports = Collections.singletonMap(transportId, properties);
 		remoteProperties = Collections.singletonMap(contactId, properties);
+		Transport remoteTransport = new Transport(transportId, remoteIndex,
+				properties);
+		remoteTransports = Collections.singletonList(remoteTransport);
 		subscriptions = Collections.singletonMap(group, 0L);
 		secret = new byte[123];
 	}
@@ -124,7 +135,7 @@ public class H2DatabaseTest extends TestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 		assertFalse(db.containsContact(txn, contactId));
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		assertTrue(db.containsContact(txn, contactId));
 		assertFalse(db.containsSubscription(txn, groupId));
 		db.addSubscription(txn, group);
@@ -142,8 +153,6 @@ public class H2DatabaseTest extends TestCase {
 		db = open(true);
 		txn = db.startTransaction();
 		assertTrue(db.containsContact(txn, contactId));
-		assertEquals(remoteProperties,
-				db.getRemoteProperties(txn, transportId));
 		assertTrue(db.containsSubscription(txn, groupId));
 		assertTrue(db.containsMessage(txn, messageId));
 		byte[] raw1 = db.getMessage(txn, messageId);
@@ -182,20 +191,20 @@ public class H2DatabaseTest extends TestCase {
 
 		// Create three contacts
 		assertFalse(db.containsContact(txn, contactId));
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		assertTrue(db.containsContact(txn, contactId));
 		assertFalse(db.containsContact(txn, contactId1));
-		assertEquals(contactId1, db.addContact(txn, transports, secret));
+		assertEquals(contactId1, db.addContact(txn, secret));
 		assertTrue(db.containsContact(txn, contactId1));
 		assertFalse(db.containsContact(txn, contactId2));
-		assertEquals(contactId2, db.addContact(txn, transports, secret));
+		assertEquals(contactId2, db.addContact(txn, secret));
 		assertTrue(db.containsContact(txn, contactId2));
 		// Delete the contact with the highest ID
 		db.removeContact(txn, contactId2);
 		assertFalse(db.containsContact(txn, contactId2));
 		// Add another contact - a new ID should be created
 		assertFalse(db.containsContact(txn, contactId3));
-		assertEquals(contactId3, db.addContact(txn, transports, secret));
+		assertEquals(contactId3, db.addContact(txn, secret));
 		assertTrue(db.containsContact(txn, contactId3));
 
 		db.commitTransaction(txn);
@@ -242,7 +251,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 
 		// Removing the contact should remove the message
@@ -261,7 +270,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 
 		// The message has no status yet, so it should not be sendable
@@ -300,7 +309,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 		db.setStatus(txn, contactId, privateMessageId, Status.NEW);
 
@@ -328,7 +337,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -366,7 +375,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -408,7 +417,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.addGroupMessage(txn, message);
@@ -447,7 +456,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.addGroupMessage(txn, message);
@@ -482,7 +491,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -513,7 +522,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
@@ -546,7 +555,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and some batches to ack
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addBatchToAck(txn, contactId, batchId);
 		db.addBatchToAck(txn, contactId, batchId1);
 
@@ -573,7 +582,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and receive the same batch twice
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addBatchToAck(txn, contactId, batchId);
 		db.addBatchToAck(txn, contactId, batchId);
 
@@ -599,7 +608,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 
@@ -624,8 +633,8 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add two contacts, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
-		ContactId contactId1 = db.addContact(txn, transports, secret);
+		assertEquals(contactId, db.addContact(txn, secret));
+		ContactId contactId1 = db.addContact(txn, secret);
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 
@@ -647,7 +656,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -686,7 +695,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -731,12 +740,12 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 
 		// Add some outstanding batches, a few ms apart
 		for(int i = 0; i < ids.length; i++) {
 			db.addOutstandingBatch(txn, contactId, ids[i],
-					Collections.<MessageId>emptySet());
+					Collections.<MessageId>emptyList());
 			Thread.sleep(5);
 		}
 
@@ -771,12 +780,12 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 
 		// Add some outstanding batches, a few ms apart
 		for(int i = 0; i < ids.length; i++) {
 			db.addOutstandingBatch(txn, contactId, ids[i],
-					Collections.<MessageId>emptySet());
+					Collections.<MessageId>emptyList());
 			Thread.sleep(5);
 		}
 
@@ -991,44 +1000,56 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact with some transport properties
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
+		db.setTransports(txn, contactId, remoteTransports, 1);
 		assertEquals(remoteProperties,
 				db.getRemoteProperties(txn, transportId));
 
 		// Replace the transport properties
 		TransportProperties properties1 =
 			new TransportProperties(Collections.singletonMap("baz", "bam"));
-		Map<TransportId, TransportProperties> transports1 =
-			Collections.singletonMap(transportId, properties1);
+		Transport remoteTransport1 =
+			new Transport(transportId, remoteIndex, properties1);
+		Collection<Transport> remoteTransports1 =
+			Collections.singletonList(remoteTransport1);
 		Map<ContactId, TransportProperties> remoteProperties1 =
 			Collections.singletonMap(contactId, properties1);
-		db.setTransports(txn, contactId, transports1, 1);
+		db.setTransports(txn, contactId, remoteTransports1, 2);
 		assertEquals(remoteProperties1,
 				db.getRemoteProperties(txn, transportId));
 
 		// Remove the transport properties
-		db.setTransports(txn, contactId,
-				Collections.<TransportId, TransportProperties>emptyMap(), 2);
+		db.setTransports(txn, contactId, Collections.<Transport>emptyList(), 3);
 		assertEquals(Collections.emptyMap(),
 				db.getRemoteProperties(txn, transportId));
 
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testLocalTransports() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Allocate a transport index
+		assertEquals(localIndex, db.addTransport(txn, transportId));
+
 		// Set the local transport properties
-		for(Entry<TransportId, TransportProperties> e : transports.entrySet()) {
-			db.setLocalProperties(txn, e.getKey(), e.getValue());
-		}
-		assertEquals(transports, db.getLocalTransports(txn));
+		db.setLocalProperties(txn, transportId, properties);
+		assertEquals(Collections.singletonList(properties),
+				db.getLocalTransports(txn));
 
-		// Remove the local transport properties
-		for(TransportId t : transports.keySet()) {
-			db.setLocalProperties(txn, t, new TransportProperties());
-		}
-		assertEquals(Collections.emptyMap(), db.getLocalTransports(txn));
+		// Remove the local transport properties - the transport itself will
+		// not be removed
+		db.setLocalProperties(txn, transportId, new TransportProperties());
+		assertEquals(Collections.singletonList(Collections.emptyMap()),
+				db.getLocalTransports(txn));
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
-
 	@Test
 	public void testUpdateTransportConfig() throws Exception {
 		TransportConfig config =
@@ -1039,6 +1060,9 @@ public class H2DatabaseTest extends TestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
+		// Allocate a transport index
+		assertEquals(localIndex, db.addTransport(txn, transportId));
+
 		// Set the transport config
 		db.setConfig(txn, transportId, config);
 		assertEquals(config, db.getConfig(txn, transportId));
@@ -1049,8 +1073,7 @@ public class H2DatabaseTest extends TestCase {
 
 		// Remove the transport config
 		db.setConfig(txn, transportId, new TransportConfig());
-		assertEquals(Collections.emptyMap(),
-				db.getConfig(txn, transportId));
+		assertEquals(Collections.emptyMap(), db.getConfig(txn, transportId));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1062,27 +1085,32 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact with some transport properties
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
+		db.setTransports(txn, contactId, remoteTransports, 1);
 		assertEquals(remoteProperties,
 				db.getRemoteProperties(txn, transportId));
 
 		// Replace the transport properties using a timestamp of 2
 		TransportProperties properties1 =
 			new TransportProperties(Collections.singletonMap("baz", "bam"));
-		Map<TransportId, TransportProperties> transports1 =
-			Collections.singletonMap(transportId, properties1);
+		Transport remoteTransport1 =
+			new Transport(transportId, remoteIndex, properties1);
+		Collection<Transport> remoteTransports1 =
+			Collections.singletonList(remoteTransport1);
 		Map<ContactId, TransportProperties> remoteProperties1 =
 			Collections.singletonMap(contactId, properties1);
-		db.setTransports(txn, contactId, transports1, 2);
+		db.setTransports(txn, contactId, remoteTransports1, 2);
 		assertEquals(remoteProperties1,
 				db.getRemoteProperties(txn, transportId));
 
 		// Try to replace the transport properties using a timestamp of 1
 		TransportProperties properties2 =
 			new TransportProperties(Collections.singletonMap("quux", "etc"));
-		Map<TransportId, TransportProperties> transports2 =
-			Collections.singletonMap(transportId, properties2);
-		db.setTransports(txn, contactId, transports2, 1);
+		Transport remoteTransport2 =
+			new Transport(transportId, remoteIndex, properties2);
+		Collection<Transport> remoteTransports2 =
+			Collections.singletonList(remoteTransport2);
+		db.setTransports(txn, contactId, remoteTransports2, 1);
 
 		// The old properties should still be there
 		assertEquals(remoteProperties1,
@@ -1100,10 +1128,8 @@ public class H2DatabaseTest extends TestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
-
-		// Add some subscriptions
+		// Add a contact with some subscriptions
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		assertEquals(Collections.singletonList(group),
 				db.getSubscriptions(txn, contactId));
@@ -1127,10 +1153,8 @@ public class H2DatabaseTest extends TestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
-
-		// Add some subscriptions
+		// Add a contact with some subscriptions
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.setSubscriptions(txn, contactId, subscriptions, 2);
 		assertEquals(Collections.singletonList(group),
 				db.getSubscriptions(txn, contactId));
@@ -1154,7 +1178,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 
@@ -1172,7 +1196,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
@@ -1195,7 +1219,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 		db.addGroupMessage(txn, message);
@@ -1218,7 +1242,7 @@ public class H2DatabaseTest extends TestCase {
 
 		// Add a contact, subscribe to a group and store a message -
 		// the message is older than the contact's subscription
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		Map<Group, Long> subs = Collections.singletonMap(group, timestamp + 1);
@@ -1242,7 +1266,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -1267,7 +1291,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -1286,7 +1310,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact with a subscription
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
 
 		// There's no local subscription for the group
@@ -1303,7 +1327,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 		db.setStatus(txn, contactId, messageId, Status.NEW);
@@ -1322,7 +1346,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -1342,7 +1366,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -1364,7 +1388,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		db.setVisibility(txn, groupId, Collections.singleton(contactId));
 		db.setSubscriptions(txn, contactId, subscriptions, 1);
@@ -1385,7 +1409,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 		// The group should not be visible to the contact
 		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
@@ -1394,7 +1418,7 @@ public class H2DatabaseTest extends TestCase {
 		assertEquals(Collections.singletonList(contactId),
 				db.getVisibility(txn, groupId));
 		// Make the group invisible again
-		db.setVisibility(txn, groupId, Collections.<ContactId>emptySet());
+		db.setVisibility(txn, groupId, Collections.<ContactId>emptyList());
 		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
 
 		db.commitTransaction(txn);
@@ -1408,10 +1432,10 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
-		// Get the connection window for a new transport
+		assertEquals(contactId, db.addContact(txn, secret));
+		// Get the connection window for a new index
 		ConnectionWindow w = db.getConnectionWindow(txn, contactId,
-				transportId);
+				remoteIndex);
 		// The connection window should exist and be in the initial state
 		assertNotNull(w);
 		assertEquals(0L, w.getCentre());
@@ -1427,19 +1451,19 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
-		// Get the connection window for a new transport
+		assertEquals(contactId, db.addContact(txn, secret));
+		// Get the connection window for a new index
 		ConnectionWindow w = db.getConnectionWindow(txn, contactId,
-				transportId);
+				remoteIndex);
 		// The connection window should exist and be in the initial state
 		assertNotNull(w);
 		assertEquals(0L, w.getCentre());
 		assertEquals(0, w.getBitmap());
 		// Update the connection window and store it
 		w.setSeen(5L);
-		db.setConnectionWindow(txn, contactId, transportId, w);
+		db.setConnectionWindow(txn, contactId, remoteIndex, w);
 		// Check that the connection window was stored
-		w = db.getConnectionWindow(txn, contactId, transportId);
+		w = db.getConnectionWindow(txn, contactId, remoteIndex);
 		assertNotNull(w);
 		assertEquals(6L, w.getCentre());
 		assertTrue(w.isSeen(5L));
@@ -1527,7 +1551,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 
 		// A message with a private parent should return null
@@ -1576,7 +1600,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 
 		// The subscription and transport timestamps should be initialised to 0
 		assertEquals(0L, db.getSubscriptionsModified(txn, contactId));
@@ -1606,7 +1630,7 @@ public class H2DatabaseTest extends TestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, transports, secret));
+		assertEquals(contactId, db.addContact(txn, secret));
 		db.addSubscription(txn, group);
 
 		// Store a couple of messages
diff --git a/test/net/sf/briar/db/TestGroup.java b/test/net/sf/briar/db/TestGroup.java
index eac3763807fced4d9333c3b8ae2aaafbf829d39d..40617162e27e380f46610f0a5e8e78e14279c893 100644
--- a/test/net/sf/briar/db/TestGroup.java
+++ b/test/net/sf/briar/db/TestGroup.java
@@ -1,10 +1,7 @@
 package net.sf.briar.db;
 
-import java.io.IOException;
-
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.serial.Writer;
 
 public class TestGroup implements Group {
 
@@ -29,8 +26,4 @@ public class TestGroup implements Group {
 	public byte[] getPublicKey() {
 		return publicKey;
 	}
-
-	public void writeTo(Writer w) throws IOException {
-		throw new UnsupportedOperationException();
-	}
 }
diff --git a/test/net/sf/briar/invitation/InvitationWorkerTest.java b/test/net/sf/briar/invitation/InvitationWorkerTest.java
index 524412b0e6976d23a39a4a029b577a01dce47589..e6dc78b87dabc4f11386ac26be8b4e28cb1b96b6 100644
--- a/test/net/sf/briar/invitation/InvitationWorkerTest.java
+++ b/test/net/sf/briar/invitation/InvitationWorkerTest.java
@@ -4,18 +4,19 @@ import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.invitation.InvitationCallback;
 import net.sf.briar.api.invitation.InvitationParameters;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
@@ -109,11 +110,12 @@ public class InvitationWorkerTest extends TestCase {
 
 	private void testInstallerCreation(final boolean createExe,
 			final boolean createJar) throws IOException, DbException {
-		TransportProperties properties =
-			new TransportProperties(Collections.singletonMap("foo", "bar"));
-		TransportId transportId = new TransportId(123);
-		final Map<TransportId, TransportProperties> transports =
-			Collections.singletonMap(transportId, properties);
+		TransportId transportId = new TransportId(TestUtils.getRandomId());
+		TransportIndex transportIndex = new TransportIndex(13);
+		Transport transport = new Transport(transportId, transportIndex,
+			Collections.singletonMap("foo", "bar"));
+		final Collection<Transport> transports =
+			Collections.singletonList(transport);
 		final File setup = new File(testDir, "setup.dat");
 		TestUtils.createFile(setup, "foo bar baz");
 		final File invitation = new File(testDir, "invitation.dat");
@@ -148,7 +150,7 @@ public class InvitationWorkerTest extends TestCase {
 			will(returnValue(transports));
 			oneOf(writerFactory).createWriter(with(any(OutputStream.class)));
 			will(returnValue(writer));
-			oneOf(writer).writeMap(transports);
+			oneOf(writer).writeList(transports);
 			oneOf(params).shouldCreateExe();
 			will(returnValue(createExe));
 			oneOf(params).shouldCreateJar();
diff --git a/test/net/sf/briar/plugins/PluginManagerImplTest.java b/test/net/sf/briar/plugins/PluginManagerImplTest.java
index 373e0678ef61263cf1dc437844ec3c72b6460529..a9f89b050dc5e3fef122bd57cf98911c30a8b8e8 100644
--- a/test/net/sf/briar/plugins/PluginManagerImplTest.java
+++ b/test/net/sf/briar/plugins/PluginManagerImplTest.java
@@ -1,11 +1,12 @@
 package net.sf.briar.plugins;
 
-import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import junit.framework.TestCase;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.ui.UiCallback;
 
@@ -22,7 +23,12 @@ public class PluginManagerImplTest extends TestCase {
 		final ConnectionDispatcher dispatcher =
 			context.mock(ConnectionDispatcher.class);
 		final UiCallback uiCallback = context.mock(UiCallback.class);
+		final AtomicInteger index = new AtomicInteger(0);
 		context.checking(new Expectations() {{
+			allowing(db).getLocalIndex(with(any(TransportId.class)));
+			will(returnValue(null));
+			allowing(db).addTransport(with(any(TransportId.class)));
+			will(returnValue(new TransportIndex(index.getAndIncrement())));
 			allowing(db).getLocalProperties(with(any(TransportId.class)));
 			will(returnValue(new TransportProperties()));
 			allowing(db).getRemoteProperties(with(any(TransportId.class)));
@@ -30,10 +36,9 @@ public class PluginManagerImplTest extends TestCase {
 			allowing(db).setLocalProperties(with(any(TransportId.class)),
 					with(any(TransportProperties.class)));
 		}});
-		Executor executor = new ImmediateExecutor();
 		Poller poller = new PollerImpl();
-		PluginManagerImpl p =
-			new PluginManagerImpl(executor, db, poller, dispatcher, uiCallback);
+		PluginManagerImpl p = new PluginManagerImpl(db, poller, dispatcher,
+				uiCallback);
 		// The Bluetooth plugin will not start without a Bluetooth device, so
 		// we expect two plugins to be started
 		assertEquals(2, p.startPlugins());
diff --git a/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java b/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
index 3cf909647896322670a2be6a81d57cc0a18b4bb9..bb174a80b05af35697077e1f234034d96cf15951 100644
--- a/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
+++ b/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
@@ -1,5 +1,7 @@
 package net.sf.briar.plugins.file;
 
+import static org.junit.Assert.assertArrayEquals;
+
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.OutputStream;
@@ -47,8 +49,8 @@ public class RemovableDrivePluginTest extends TestCase {
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
 				callback, finder, monitor);
 
-		assertEquals(RemovableDrivePlugin.TRANSPORT_ID,
-				plugin.getId().getInt());
+		assertArrayEquals(RemovableDrivePlugin.TRANSPORT_ID,
+				plugin.getId().getBytes());
 
 		context.assertIsSatisfied();
 	}
diff --git a/test/net/sf/briar/protocol/ProtocolReadWriteTest.java b/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
index c502c27b081bea45422513aacc053a6243b430d5..5efbfdc1aa6f471614be768e8f07c51f14ce8bc0 100644
--- a/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
+++ b/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
@@ -3,28 +3,30 @@ package net.sf.briar.protocol;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.util.BitSet;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.protocol.writers.OfferWriter;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.writers.RequestWriter;
@@ -50,8 +52,7 @@ public class ProtocolReadWriteTest extends TestCase {
 	private final String messageBody = "Hello world";
 	private final BitSet bitSet;
 	private final Map<Group, Long> subscriptions;
-	private final TransportId transportId;
-	private final Map<TransportId, TransportProperties> transports;
+	private final Collection<Transport> transports;
 	private final long timestamp = System.currentTimeMillis();
 
 	public ProtocolReadWriteTest() throws Exception {
@@ -71,10 +72,11 @@ public class ProtocolReadWriteTest extends TestCase {
 		bitSet.set(3);
 		bitSet.set(7);
 		subscriptions = Collections.singletonMap(group, 123L);
-		transportId = new TransportId(123);
-		TransportProperties p =
-			new TransportProperties(Collections.singletonMap("bar", "baz"));
-		transports = Collections.singletonMap(transportId, p);
+		TransportId transportId = new TransportId(TestUtils.getRandomId());
+		TransportIndex transportIndex = new TransportIndex(13);
+		Transport transport = new Transport(transportId, transportIndex,
+				Collections.singletonMap("bar", "baz"));
+		transports = Collections.singletonList(transport);
 	}
 
 	@Test
diff --git a/test/net/sf/briar/protocol/writers/ConstantsTest.java b/test/net/sf/briar/protocol/writers/ConstantsTest.java
index 372d32dc48d0b551bc328a46350c8b21a1d50798..d158040b39435f3e1796c11502731bc5efa9c740 100644
--- a/test/net/sf/briar/protocol/writers/ConstantsTest.java
+++ b/test/net/sf/briar/protocol/writers/ConstantsTest.java
@@ -1,14 +1,25 @@
 package net.sf.briar.protocol.writers;
 
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_AUTHOR_NAME_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_BODY_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_GROUPS;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_GROUP_NAME_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PUBLIC_KEY_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_SUBJECT_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_TRANSPORTS;
+
 import java.io.ByteArrayOutputStream;
 import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
@@ -16,14 +27,14 @@ import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.ProtocolConstants;
-import net.sf.briar.api.protocol.SubscriptionUpdate;
-import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.protocol.writers.AckWriter;
 import net.sf.briar.api.protocol.writers.BatchWriter;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.protocol.writers.OfferWriter;
 import net.sf.briar.api.protocol.writers.SubscriptionWriter;
 import net.sf.briar.api.protocol.writers.TransportWriter;
@@ -50,7 +61,8 @@ public class ConstantsTest extends TestCase {
 	public ConstantsTest() throws Exception {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule(),
-				new ProtocolModule(), new SerialModule());
+				new ProtocolModule(), new ProtocolWritersModule(),
+				new SerialModule());
 		writerFactory = i.getInstance(WriterFactory.class);
 		crypto = i.getInstance(CryptoComponent.class);
 		serial = i.getInstance(SerialComponent.class);
@@ -61,7 +73,7 @@ public class ConstantsTest extends TestCase {
 
 	@Test
 	public void testBatchesFitIntoLargeAck() throws Exception {
-		testBatchesFitIntoAck(ProtocolConstants.MAX_PACKET_LENGTH);
+		testBatchesFitIntoAck(MAX_PACKET_LENGTH);
 	}
 
 	@Test
@@ -94,32 +106,32 @@ public class ConstantsTest extends TestCase {
 	@Test
 	public void testMessageFitsIntoBatch() throws Exception {
 		// Create a maximum-length group
-		String groupName = createRandomString(Group.MAX_NAME_LENGTH);
-		byte[] groupPublic = new byte[Group.MAX_PUBLIC_KEY_LENGTH];
+		String groupName = createRandomString(MAX_GROUP_NAME_LENGTH);
+		byte[] groupPublic = new byte[MAX_PUBLIC_KEY_LENGTH];
 		Group group = groupFactory.createGroup(groupName, groupPublic);
 		// Create a maximum-length author
-		String authorName = createRandomString(Author.MAX_NAME_LENGTH);
-		byte[] authorPublic = new byte[Author.MAX_PUBLIC_KEY_LENGTH];
+		String authorName = createRandomString(MAX_AUTHOR_NAME_LENGTH);
+		byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH];
 		Author author = authorFactory.createAuthor(authorName, authorPublic);
 		// Create a maximum-length message
 		PrivateKey groupPrivate = crypto.generateKeyPair().getPrivate();
 		PrivateKey authorPrivate = crypto.generateKeyPair().getPrivate();
-		String subject = createRandomString(Message.MAX_SUBJECT_LENGTH);
-		byte[] body = new byte[Message.MAX_BODY_LENGTH];
+		String subject = createRandomString(MAX_SUBJECT_LENGTH);
+		byte[] body = new byte[MAX_BODY_LENGTH];
 		Message message = messageEncoder.encodeMessage(null, group,
 				groupPrivate, author, authorPrivate, subject, body);
 		// Add the message to a batch
-		ByteArrayOutputStream out = new ByteArrayOutputStream(
-				ProtocolConstants.MAX_PACKET_LENGTH);
+		ByteArrayOutputStream out =
+			new ByteArrayOutputStream(MAX_PACKET_LENGTH);
 		BatchWriter b = new BatchWriterImpl(out, serial, writerFactory,
 				crypto.getMessageDigest());
 		assertTrue(b.writeMessage(message.getSerialised()));
 		b.finish();
 		// Check the size of the serialised batch
-		assertTrue(out.size() > UniqueId.LENGTH + Group.MAX_NAME_LENGTH +
-				Group.MAX_PUBLIC_KEY_LENGTH + Author.MAX_NAME_LENGTH +
-				Author.MAX_PUBLIC_KEY_LENGTH + Message.MAX_BODY_LENGTH);
-		assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
+		assertTrue(out.size() > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH +
+				MAX_PUBLIC_KEY_LENGTH + MAX_AUTHOR_NAME_LENGTH +
+				MAX_PUBLIC_KEY_LENGTH + MAX_BODY_LENGTH);
+		assertTrue(out.size() <= MAX_PACKET_LENGTH);
 	}
 
 	@Test
@@ -136,7 +148,7 @@ public class ConstantsTest extends TestCase {
 
 	@Test
 	public void testMessagesFitIntoLargeOffer() throws Exception {
-		testMessagesFitIntoOffer(ProtocolConstants.MAX_PACKET_LENGTH);
+		testMessagesFitIntoOffer(MAX_PACKET_LENGTH);
 	}
 
 	@Test
@@ -169,52 +181,49 @@ public class ConstantsTest extends TestCase {
 	@Test
 	public void testSubscriptionsFitIntoUpdate() throws Exception {
 		// Create the maximum number of maximum-length subscriptions
-		Map<Group, Long> subs =
-			new HashMap<Group, Long>(SubscriptionUpdate.MAX_SUBS_PER_UPDATE);
-		byte[] publicKey = new byte[Group.MAX_PUBLIC_KEY_LENGTH];
-		for(int i = 0; i < SubscriptionUpdate.MAX_SUBS_PER_UPDATE; i++) {
-			String name = createRandomString(Group.MAX_NAME_LENGTH);
+		Map<Group, Long> subs = new HashMap<Group, Long>(MAX_GROUPS);
+		byte[] publicKey = new byte[MAX_PUBLIC_KEY_LENGTH];
+		for(int i = 0; i < MAX_GROUPS; i++) {
+			String name = createRandomString(MAX_GROUP_NAME_LENGTH);
 			Group group = groupFactory.createGroup(name, publicKey);
 			subs.put(group, Long.MAX_VALUE);
 		}
 		// Add the subscriptions to an update
-		ByteArrayOutputStream out = new ByteArrayOutputStream(
-				ProtocolConstants.MAX_PACKET_LENGTH);
+		ByteArrayOutputStream out =
+			new ByteArrayOutputStream(MAX_PACKET_LENGTH);
 		SubscriptionWriter s = new SubscriptionWriterImpl(out, writerFactory);
 		s.writeSubscriptions(subs, Long.MAX_VALUE);
 		// Check the size of the serialised update
-		assertTrue(out.size() > SubscriptionUpdate.MAX_SUBS_PER_UPDATE *
-				(Group.MAX_NAME_LENGTH + Group.MAX_PUBLIC_KEY_LENGTH + 8) + 8);
-		assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
+		assertTrue(out.size() > MAX_GROUPS *
+				(MAX_GROUP_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH + 8) + 8);
+		assertTrue(out.size() <= MAX_PACKET_LENGTH);
 	}
 
 	@Test
 	public void testTransportsFitIntoUpdate() throws Exception {
 		// Create the maximum number of plugins, each with the maximum number
 		// of maximum-length properties
-		Map<TransportId, TransportProperties> transports =
-			new HashMap<TransportId, TransportProperties>();
-		for(int i = 0; i < TransportUpdate.MAX_PLUGINS_PER_UPDATE; i++) {
-			TransportProperties p = new TransportProperties();
-			for(int j = 0; j < TransportUpdate.MAX_PROPERTIES_PER_PLUGIN; j++) {
-				String key = createRandomString(
-						TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
-				String value = createRandomString(
-						TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
-				p.put(key, value);
+		Collection<Transport> transports = new ArrayList<Transport>();
+		for(int i = 0; i < MAX_TRANSPORTS; i++) {
+			TransportId id = new TransportId(TestUtils.getRandomId());
+			TransportIndex index = new TransportIndex(i);
+			Transport t = new Transport(id, index);
+			for(int j = 0; j < MAX_PROPERTIES_PER_TRANSPORT; j++) {
+				String key = createRandomString(MAX_PROPERTY_LENGTH);
+				String value = createRandomString(MAX_PROPERTY_LENGTH);
+				t.put(key, value);
 			}
-			transports.put(new TransportId(i), p);
+			transports.add(t);
 		}
 		// Add the transports to an update
-		ByteArrayOutputStream out = new ByteArrayOutputStream(
-				ProtocolConstants.MAX_PACKET_LENGTH);
+		ByteArrayOutputStream out =
+			new ByteArrayOutputStream(MAX_PACKET_LENGTH);
 		TransportWriter t = new TransportWriterImpl(out, writerFactory);
 		t.writeTransports(transports, Long.MAX_VALUE);
 		// Check the size of the serialised update
-		assertTrue(out.size() > TransportUpdate.MAX_PLUGINS_PER_UPDATE *
-				(4 + TransportUpdate.MAX_PROPERTIES_PER_PLUGIN *
-						TransportUpdate.MAX_KEY_OR_VALUE_LENGTH * 2) + 8);
-		assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
+		assertTrue(out.size() > MAX_TRANSPORTS * (UniqueId.LENGTH + 4 +
+				(MAX_PROPERTIES_PER_TRANSPORT * MAX_PROPERTY_LENGTH * 2)) + 8);
+		assertTrue(out.size() <= MAX_PACKET_LENGTH);
 	}
 
 	private static String createRandomString(int length) throws Exception {
diff --git a/test/net/sf/briar/protocol/writers/RequestWriterImplTest.java b/test/net/sf/briar/protocol/writers/RequestWriterImplTest.java
index e017074d3ea52841cb25e35d8cba5dd6da21f803..93ed7180c8b3ab74220536469acb6237046c72f7 100644
--- a/test/net/sf/briar/protocol/writers/RequestWriterImplTest.java
+++ b/test/net/sf/briar/protocol/writers/RequestWriterImplTest.java
@@ -42,9 +42,9 @@ public class RequestWriterImplTest extends TestCase {
 		b.set(12);
 		b.set(15);
 		r.writeRequest(b, 16);
-		// Short user tag 10, short bytes with length 2, 0xD959
+		// Short user tag 8, short bytes with length 2, 0xD959
 		byte[] output = out.toByteArray();
-		assertEquals("CA" + "92" + "D959", StringUtils.toHexString(output));
+		assertEquals("C8" + "92" + "D959", StringUtils.toHexString(output));
 	}
 
 	@Test
@@ -63,8 +63,8 @@ public class RequestWriterImplTest extends TestCase {
 		b.set(11);
 		b.set(12);
 		r.writeRequest(b, 13);
-		// Short user tag 10, short bytes with length 2, 0x59D8
+		// Short user tag 8, short bytes with length 2, 0x59D8
 		byte[] output = out.toByteArray();
-		assertEquals("CA" + "92" + "59D8", StringUtils.toHexString(output));
+		assertEquals("C8" + "92" + "59D8", StringUtils.toHexString(output));
 	}
 }
diff --git a/test/net/sf/briar/serial/WriterImplTest.java b/test/net/sf/briar/serial/WriterImplTest.java
index 8bd773a2e9cc1a3fe4a4a4e8b3de55edd7567a9c..7fe9b285072144c97dad2a30b367d690715b2404 100644
--- a/test/net/sf/briar/serial/WriterImplTest.java
+++ b/test/net/sf/briar/serial/WriterImplTest.java
@@ -4,14 +4,11 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
 import junit.framework.TestCase;
-import net.sf.briar.api.serial.Writable;
-import net.sf.briar.api.serial.Writer;
 import net.sf.briar.util.StringUtils;
 
 import org.junit.Before;
@@ -284,20 +281,6 @@ public class WriterImplTest extends TestCase {
 		checkContents("EF" + "20" + "EF" + "FF");
 	}
 
-	@Test
-	public void testWriteCollectionOfWritables() throws IOException {
-		Writable writable = new Writable() {
-			public void writeTo(Writer w) throws IOException {
-				w.writeUserDefinedId(0);
-				w.writeString("foo");
-			}
-		};
-		w.writeList(Collections.singleton(writable));
-		// SHORT_LIST tag, length 1, SHORT_USER tag (3 bits), 0 (5 bits),
-		// "foo" as short string
-		checkContents("A" + "1" + "C0" + "83666F6F");
-	}
-
 	private void checkContents(String hex) throws IOException {
 		out.flush();
 		out.close();
diff --git a/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java b/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
index dc3cbb4c33ccb4ba49bcde2ad59828a2d59b373d..20361bac56f93cdf7492eb290efcf760a4fcb82c 100644
--- a/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
@@ -11,8 +11,8 @@ import javax.crypto.spec.IvParameterSpec;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.apache.commons.io.output.ByteArrayOutputStream;
@@ -27,7 +27,7 @@ public class ConnectionDecrypterImplTest extends TestCase {
 
 	private final Cipher ivCipher, frameCipher;
 	private final SecretKey ivKey, frameKey;
-	private final TransportId transportId = new TransportId(123);
+	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final long connection = 12345L;
 
 	public ConnectionDecrypterImplTest() {
@@ -52,7 +52,7 @@ public class ConnectionDecrypterImplTest extends TestCase {
 
 	private void testDecryption(boolean initiator) throws Exception {
 		// Calculate the plaintext and ciphertext for the IV
-		byte[] iv = IvEncoder.encodeIv(initiator, transportId, connection);
+		byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
 		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
 		byte[] encryptedIv  = ivCipher.doFinal(iv);
 		assertEquals(IV_LENGTH, encryptedIv.length);
@@ -85,7 +85,7 @@ public class ConnectionDecrypterImplTest extends TestCase {
 		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
 		// Use a ConnectionDecrypter to decrypt the ciphertext
 		ConnectionDecrypter d = new ConnectionDecrypterImpl(in, 
-				IvEncoder.encodeIv(initiator, transportId, connection),
+				IvEncoder.encodeIv(initiator, transportIndex, connection),
 				frameCipher, frameKey);
 		// First frame
 		byte[] decrypted = new byte[ciphertext.length];
diff --git a/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java b/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
index 3f81230dbcdaa7c92fe1ab1cb493845850079e95..9f191da7f2e67880f2bdb4002ed9de1ea04f2547 100644
--- a/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
@@ -10,8 +10,8 @@ import javax.crypto.SecretKey;
 import javax.crypto.spec.IvParameterSpec;
 
 import junit.framework.TestCase;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.junit.Test;
@@ -25,7 +25,7 @@ public class ConnectionEncrypterImplTest extends TestCase {
 
 	private final Cipher ivCipher, frameCipher;
 	private final SecretKey ivKey, frameKey;
-	private final TransportId transportId = new TransportId(123);
+	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final long connection = 12345L;
 
 	public ConnectionEncrypterImplTest() {
@@ -50,7 +50,7 @@ public class ConnectionEncrypterImplTest extends TestCase {
 
 	private void testEncryption(boolean initiator) throws Exception {
 		// Calculate the expected ciphertext for the IV
-		byte[] iv = IvEncoder.encodeIv(initiator, transportId, connection);
+		byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
 		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
 		byte[] encryptedIv = ivCipher.doFinal(iv);
 		assertEquals(IV_LENGTH, encryptedIv.length);
@@ -82,7 +82,7 @@ public class ConnectionEncrypterImplTest extends TestCase {
 		byte[] expected = out.toByteArray();
 		// Use a ConnectionEncrypter to encrypt the plaintext
 		out.reset();
-		iv = IvEncoder.encodeIv(initiator, transportId, connection);
+		iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
 		ConnectionEncrypter e = new ConnectionEncrypterImpl(out, Long.MAX_VALUE,
 				iv, ivCipher, frameCipher, ivKey, frameKey);
 		e.getOutputStream().write(plaintext);
diff --git a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
index bd5d1b0cef48542f557d7ccc3b636883353137de..b95460a59e29c5bb0aefeacbab3d8a4a1bb58f25 100644
--- a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
@@ -9,10 +9,14 @@ import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
 
 import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionWindow;
 import net.sf.briar.crypto.CryptoModule;
 
@@ -29,6 +33,8 @@ public class ConnectionRecogniserImplTest extends TestCase {
 	private final ContactId contactId;
 	private final byte[] secret;
 	private final TransportId transportId;
+	private final TransportIndex localIndex, remoteIndex;
+	private final Collection<Transport> transports;
 	private final ConnectionWindow connectionWindow;
 
 	public ConnectionRecogniserImplTest() {
@@ -37,7 +43,12 @@ public class ConnectionRecogniserImplTest extends TestCase {
 		crypto = i.getInstance(CryptoComponent.class);
 		contactId = new ContactId(1);
 		secret = new byte[18];
-		transportId = new TransportId(123);
+		transportId = new TransportId(TestUtils.getRandomId());
+		localIndex = new TransportIndex(13);
+		remoteIndex = new TransportIndex(7);
+		Transport transport = new Transport(transportId, localIndex,
+				Collections.singletonMap("foo", "bar"));
+		transports = Collections.singletonList(transport);
 		connectionWindow = new ConnectionWindowImpl(0L, 0);
 	}
 
@@ -47,15 +58,20 @@ public class ConnectionRecogniserImplTest extends TestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
 			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			// Initialise
+			oneOf(db).getLocalTransports();
+			will(returnValue(transports));
 			oneOf(db).getContacts();
 			will(returnValue(Collections.singleton(contactId)));
 			oneOf(db).getSharedSecret(contactId);
 			will(returnValue(secret));
-			oneOf(db).getConnectionWindow(contactId, transportId);
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
 			will(returnValue(connectionWindow));
 		}});
 		final ConnectionRecogniserImpl c =
-			new ConnectionRecogniserImpl(transportId, crypto, db);
+			new ConnectionRecogniserImpl(crypto, db);
 		assertNull(c.acceptConnection(new byte[IV_LENGTH]));
 		context.assertIsSatisfied();
 	}
@@ -66,26 +82,41 @@ public class ConnectionRecogniserImplTest extends TestCase {
 		SecretKey ivKey = crypto.deriveIncomingIvKey(secret);
 		Cipher ivCipher = crypto.getIvCipher();
 		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
-		byte[] iv = IvEncoder.encodeIv(true, transportId, 3L);
+		byte[] iv = IvEncoder.encodeIv(true, remoteIndex, 3L);
 		byte[] encryptedIv = ivCipher.doFinal(iv);
 
 		Mockery context = new Mockery();
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
 			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			// Initialise
+			oneOf(db).getLocalTransports();
+			will(returnValue(transports));
 			oneOf(db).getContacts();
 			will(returnValue(Collections.singleton(contactId)));
 			oneOf(db).getSharedSecret(contactId);
 			will(returnValue(secret));
-			oneOf(db).getConnectionWindow(contactId, transportId);
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
 			will(returnValue(connectionWindow));
-			oneOf(db).setConnectionWindow(contactId, transportId,
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(connectionWindow));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex,
 					connectionWindow);
+			oneOf(db).getSharedSecret(contactId);
+			will(returnValue(secret));
 		}});
 		final ConnectionRecogniserImpl c =
-			new ConnectionRecogniserImpl(transportId, crypto, db);
+			new ConnectionRecogniserImpl(crypto, db);
 		// First time - the IV should be expected
-		assertEquals(contactId, c.acceptConnection(encryptedIv));
+		ConnectionContext ctx = c.acceptConnection(encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(transportId, ctx.getTransportId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3L, ctx.getConnectionNumber());
 		// Second time - the IV should no longer be expected
 		assertNull(c.acceptConnection(encryptedIv));
 		// The window should have advanced
diff --git a/test/net/sf/briar/transport/ConnectionWriterTest.java b/test/net/sf/briar/transport/ConnectionWriterTest.java
index 6640628089f85bb8726e1bdedff954736819ffad..e62318701783b21c83ac2eefbbced5b35cd7dd42 100644
--- a/test/net/sf/briar/transport/ConnectionWriterTest.java
+++ b/test/net/sf/briar/transport/ConnectionWriterTest.java
@@ -7,13 +7,16 @@ import java.io.ByteArrayOutputStream;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestDatabaseModule;
-import net.sf.briar.api.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.db.DatabaseModule;
 import net.sf.briar.protocol.ProtocolModule;
+import net.sf.briar.protocol.writers.ProtocolWritersModule;
 import net.sf.briar.serial.SerialModule;
+import net.sf.briar.transport.batch.TransportBatchModule;
+import net.sf.briar.transport.stream.TransportStreamModule;
 
 import org.junit.Test;
 
@@ -24,14 +27,16 @@ public class ConnectionWriterTest extends TestCase {
 
 	private final ConnectionWriterFactory connectionWriterFactory;
 	private final byte[] secret = new byte[100];
-	private final TransportId transportId = new TransportId(123);
+	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final long connection = 12345L;
 
 	public ConnectionWriterTest() throws Exception {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule(),
-				new DatabaseModule(), new ProtocolModule(), new SerialModule(),
-				new TestDatabaseModule(), new TransportModule());
+				new DatabaseModule(), new ProtocolModule(),
+				new ProtocolWritersModule(), new SerialModule(),
+				new TestDatabaseModule(), new TransportBatchModule(),
+				new TransportModule(), new TransportStreamModule());
 		connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
 	}
 
@@ -40,7 +45,7 @@ public class ConnectionWriterTest extends TestCase {
 		ByteArrayOutputStream out =
 			new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
 		ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
-				MIN_CONNECTION_LENGTH, transportId, connection, secret);
+				MIN_CONNECTION_LENGTH, transportIndex, connection, secret);
 		// Check that the connection writer thinks there's room for a packet
 		long capacity = w.getRemainingCapacity();
 		assertTrue(capacity >= MAX_PACKET_LENGTH);
diff --git a/test/net/sf/briar/transport/FrameReadWriteTest.java b/test/net/sf/briar/transport/FrameReadWriteTest.java
index 8ed227dd58fdf3b45f9b8fb850b2bfef0c36ec9e..03d61462254b92cf84a359c2b6947c2b49d13849 100644
--- a/test/net/sf/briar/transport/FrameReadWriteTest.java
+++ b/test/net/sf/briar/transport/FrameReadWriteTest.java
@@ -14,8 +14,8 @@ import javax.crypto.Mac;
 import javax.crypto.SecretKey;
 
 import junit.framework.TestCase;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.crypto.CryptoModule;
@@ -33,7 +33,7 @@ public class FrameReadWriteTest extends TestCase {
 	private final Mac mac;
 	private final Random random;
 	private final byte[] secret = new byte[100];
-	private final TransportId transportId = new TransportId(123);
+	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final long connection = 12345L;
 
 	public FrameReadWriteTest() {
@@ -62,7 +62,7 @@ public class FrameReadWriteTest extends TestCase {
 
 	private void testWriteAndRead(boolean initiator) throws Exception {
 		// Create and encrypt the IV
-		byte[] iv = IvEncoder.encodeIv(initiator, transportId, connection);
+		byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
 		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
 		byte[] encryptedIv = ivCipher.doFinal(iv);
 		assertEquals(IV_LENGTH, encryptedIv.length);
@@ -90,7 +90,7 @@ public class FrameReadWriteTest extends TestCase {
 		// Decrypt the IV
 		ivCipher.init(Cipher.DECRYPT_MODE, ivKey);
 		byte[] recoveredIv = ivCipher.doFinal(recoveredEncryptedIv);
-		iv = IvEncoder.encodeIv(initiator, transportId, connection);
+		iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
 		assertArrayEquals(iv, recoveredIv);
 		// Read the frames back
 		ConnectionDecrypter decrypter = new ConnectionDecrypterImpl(in, iv,
diff --git a/test/net/sf/briar/transport/batch/BatchConnectionReadWriteTest.java b/test/net/sf/briar/transport/batch/BatchConnectionReadWriteTest.java
index b6d0ab5e80a75338d202acbd69ef00cd74c8602c..c3e17d8d31891fdc9fe7e5fa507dc49c3d05c820 100644
--- a/test/net/sf/briar/transport/batch/BatchConnectionReadWriteTest.java
+++ b/test/net/sf/briar/transport/batch/BatchConnectionReadWriteTest.java
@@ -7,28 +7,30 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.Map;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestDatabaseModule;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
-import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.writers.MessageEncoder;
 import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.transport.BatchTransportReader;
 import net.sf.briar.api.transport.BatchTransportWriter;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.api.transport.ConnectionRecogniser;
-import net.sf.briar.api.transport.ConnectionRecogniserFactory;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.db.DatabaseModule;
@@ -36,6 +38,7 @@ import net.sf.briar.protocol.ProtocolModule;
 import net.sf.briar.protocol.writers.ProtocolWritersModule;
 import net.sf.briar.serial.SerialModule;
 import net.sf.briar.transport.TransportModule;
+import net.sf.briar.transport.stream.TransportStreamModule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -49,15 +52,16 @@ public class BatchConnectionReadWriteTest extends TestCase {
 	private final File testDir = TestUtils.getTestDirectory();
 	private final File aliceDir = new File(testDir, "alice");
 	private final File bobDir = new File(testDir, "bob");
-	private final TransportId transportId = new TransportId(123);
-	private final Map<TransportId, TransportProperties> transports =
-		Collections.emptyMap();
+	private final TransportId transportId;
+	private final TransportIndex transportIndex;
 	private final byte[] aliceSecret, bobSecret;
 
 	private Injector alice, bob;
 
 	public BatchConnectionReadWriteTest() throws Exception {
 		super();
+		transportId = new TransportId(TestUtils.getRandomId());
+		transportIndex = new TransportIndex(1);
 		// Create matching secrets for Alice and Bob
 		aliceSecret = new byte[100];
 		aliceSecret[16] = (byte) 1;
@@ -71,12 +75,14 @@ public class BatchConnectionReadWriteTest extends TestCase {
 		alice = Guice.createInjector(new CryptoModule(), new DatabaseModule(),
 				new ProtocolModule(), new ProtocolWritersModule(),
 				new SerialModule(), new TestDatabaseModule(aliceDir),
-				new TransportModule());
+				new TransportBatchModule(), new TransportModule(),
+				new TransportStreamModule());
 		// Create Bob's injector
 		bob = Guice.createInjector(new CryptoModule(), new DatabaseModule(),
 				new ProtocolModule(), new ProtocolWritersModule(),
 				new SerialModule(), new TestDatabaseModule(bobDir),
-				new TransportModule());
+				new TransportBatchModule(), new TransportModule(),
+				new TransportStreamModule());
 	}
 
 	@Test
@@ -96,7 +102,7 @@ public class BatchConnectionReadWriteTest extends TestCase {
 		DatabaseComponent db = alice.getInstance(DatabaseComponent.class);
 		db.open(false);
 		// Add Bob as a contact and send him a message
-		ContactId contactId = db.addContact(transports, aliceSecret);
+		ContactId contactId = db.addContact(aliceSecret);
 		String subject = "Hello";
 		byte[] messageBody = "Hi Bob!".getBytes("UTF-8");
 		MessageEncoder encoder = alice.getInstance(MessageEncoder.class);
@@ -110,7 +116,8 @@ public class BatchConnectionReadWriteTest extends TestCase {
 			alice.getInstance(ProtocolWriterFactory.class);
 		BatchTransportWriter writer = new TestBatchTransportWriter(out);
 		OutgoingBatchConnection batchOut = new OutgoingBatchConnection(
-				connFactory, db, protoFactory, transportId, contactId, writer);
+				connFactory, db, protoFactory, transportIndex, contactId,
+				writer);
 		// Write whatever needs to be written
 		batchOut.write();
 		// Close Alice's database
@@ -127,18 +134,33 @@ public class BatchConnectionReadWriteTest extends TestCase {
 		MessageListener listener = new MessageListener();
 		db.addListener(listener);
 		// Add Alice as a contact
-		ContactId contactId = db.addContact(transports, bobSecret);
+		ContactId contactId = db.addContact(bobSecret);
+		// Add the transport
+		assertEquals(transportIndex, db.addTransport(transportId));
+		// Fake a transport update from Alice
+		TransportUpdate transportUpdate = new TransportUpdate() {
+
+			public Collection<Transport> getTransports() {
+				Transport t = new Transport(transportId, transportIndex);
+				return Collections.singletonList(t);
+			}
+
+			public long getTimestamp() {
+				return System.currentTimeMillis();
+			}
+		};
+		db.receiveTransportUpdate(contactId, transportUpdate);
 		// Create a connection recogniser and recognise the connection
 		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		ConnectionRecogniserFactory recFactory =
-			bob.getInstance(ConnectionRecogniserFactory.class);
-		ConnectionRecogniser rec =
-			recFactory.createConnectionRecogniser(transportId);
+		ConnectionRecogniser rec = bob.getInstance(ConnectionRecogniser.class);
 		byte[] encryptedIv = new byte[IV_LENGTH];
 		int read = in.read(encryptedIv);
 		assertEquals(encryptedIv.length, read);
-		ContactId accepted = rec.acceptConnection(encryptedIv);
-		assertEquals(contactId, accepted);
+		ConnectionContext ctx = rec.acceptConnection(encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(transportId, ctx.getTransportId());
+		assertEquals(transportIndex, ctx.getTransportIndex());
 		// Create an incoming batch connection
 		ConnectionReaderFactory connFactory =
 			bob.getInstance(ConnectionReaderFactory.class);
@@ -146,8 +168,8 @@ public class BatchConnectionReadWriteTest extends TestCase {
 			bob.getInstance(ProtocolReaderFactory.class);
 		BatchTransportReader reader = new TestBatchTransportReader(in);
 		IncomingBatchConnection batchIn = new IncomingBatchConnection(
-				connFactory, db, protoFactory, transportId, contactId, reader,
-				encryptedIv);
+				connFactory, db, protoFactory, transportIndex, contactId,
+				reader, encryptedIv);
 		// No messages should have been added yet
 		assertFalse(listener.messagesAdded);
 		// Read whatever needs to be read