diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index e67f0cb912690bdcd529dbdaa5575caa935d425b..c1dfc0072419a3b80b1ead10f4222926de3fb0d4 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -158,6 +158,9 @@ public interface DatabaseComponent {
 	/** Returns the group with the given ID, if the user subscribes to it. */
 	Group getGroup(GroupId g) throws DbException;
 
+	/** Returns the metadata for the given group. */
+	Metadata getGroupMetadata(GroupId g) throws DbException;
+
 	/**
 	 * Returns all groups belonging to the given client to which the user
 	 * subscribes.
@@ -234,6 +237,12 @@ public interface DatabaseComponent {
 	void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
 			throws DbException;
 
+	/**
+	 * Merges the given metadata with the existing metadata for the given
+	 * group.
+	 */
+	void mergeGroupMetadata(GroupId g, Metadata meta) throws DbException;
+
 	/**
 	 * Merges the given properties with the existing local properties for the
 	 * given transport.
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index 30e1754530c28d6a8e622dcdf0968b9c661b1d33..01a7b54bfcfcd1399e6798a4e4afdb15d865b5cb 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -27,8 +27,6 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
 
-// FIXME: Document the preconditions for calling each method
-
 /**
  * A low-level interface to the database (DatabaseComponent provides a
  * high-level interface). Most operations take a transaction argument, which is
@@ -275,6 +273,13 @@ interface Database<T> {
 	 */
 	Group getGroup(T txn, GroupId g) throws DbException;
 
+	/**
+	 * Returns the metadata for the given group.
+	 * <p>
+	 * Locking: read.
+	 */
+	Metadata getGroupMetadata(T txn, GroupId g) throws DbException;
+
 	/**
 	 * Returns all groups belonging to the given client to which the user
 	 * subscribes.
@@ -515,6 +520,15 @@ interface Database<T> {
 	void lowerRequestedFlag(T txn, ContactId c, Collection<MessageId> requested)
 			throws DbException;
 
+	/*
+	 * Merges the given metadata with the existing metadata for the given
+	 * group.
+	 * <p>
+	 * Locking: write.
+	 */
+	void mergeGroupMetadata(T txn, GroupId g, Metadata meta)
+			throws DbException;
+
 	/**
 	 * Merges the given properties with the existing local properties for the
 	 * given transport.
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 07aaee5ccd879fae76b1ef43c77c030e044df8b9..85ff594e6c38893fa66e2702dc09411e5d91f0f4 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -607,6 +607,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
+	public Metadata getGroupMetadata(GroupId g) throws DbException {
+		lock.readLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if (!db.containsGroup(txn, g))
+					throw new NoSuchSubscriptionException();
+				Metadata metadata = db.getGroupMetadata(txn, g);
+				db.commitTransaction(txn);
+				return metadata;
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
 	public Collection<Group> getGroups(ClientId c) throws DbException {
 		lock.readLock().lock();
 		try {
@@ -954,6 +973,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
+	public void mergeGroupMetadata(GroupId g, Metadata meta)
+			throws DbException {
+		lock.writeLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if (!db.containsGroup(txn, g))
+					throw new NoSuchSubscriptionException();
+				db.mergeGroupMetadata(txn, g, meta);
+				db.commitTransaction(txn);
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
 	public void mergeLocalProperties(TransportId t, TransportProperties p)
 			throws DbException {
 		boolean changed = false;
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index c2ea25a9ce72e173f14d1022589fbd63b91a9f1f..69ed999735fafb8987e616884538afca71765a2c 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -65,8 +65,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 16;
-	private static final int MIN_SCHEMA_VERSION = 16;
+	private static final int SCHEMA_VERSION = 17;
+	private static final int MIN_SCHEMA_VERSION = 17;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -107,6 +107,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " visibleToAll BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (groupId))";
 
+	private static final String CREATE_GROUP_METADATA =
+			"CREATE TABLE groupMetadata"
+					+ " (groupId HASH NOT NULL,"
+					+ " key VARCHAR NOT NULL,"
+					+ " value BINARY NOT NULL,"
+					+ " PRIMARY KEY (groupId, key),"
+					+ " FOREIGN KEY (groupId)"
+					+ " REFERENCES groups (groupId)"
+					+ " ON DELETE CASCADE)";
+
 	private static final String CREATE_GROUP_VISIBILITIES =
 			"CREATE TABLE groupVisibilities"
 					+ " (contactId INT NOT NULL,"
@@ -386,6 +396,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_LOCAL_AUTHORS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACTS));
 			s.executeUpdate(insertTypeNames(CREATE_GROUPS));
+			s.executeUpdate(insertTypeNames(CREATE_GROUP_METADATA));
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS));
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS));
@@ -1496,16 +1507,25 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Metadata getGroupMetadata(Connection txn, GroupId g)
+			throws DbException {
+		return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId");
+	}
+
 	public Metadata getMessageMetadata(Connection txn, MessageId m)
 			throws DbException {
+		return getMetadata(txn, m.getBytes(), "messageMetadata", "messageId");
+	}
+
+	private Metadata getMetadata(Connection txn, byte[] id, String tableName,
+			String columnName) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value"
-					+ " FROM messageMetadata"
-					+ " WHERE messageId = ?";
+			String sql = "SELECT key, value FROM " + tableName
+					+ " WHERE " + columnName + " = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, m.getBytes());
+			ps.setBytes(1, id);
 			rs = ps.executeQuery();
 			Metadata metadata = new Metadata();
 			while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2));
@@ -2329,8 +2349,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
+			throws DbException {
+		mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId");
+	}
+
 	public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta)
 			throws DbException {
+		mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId");
+	}
+
+	private void mergeMetadata(Connection txn, byte[] id, Metadata meta,
+			String tableName, String columnName) throws DbException {
 		PreparedStatement ps = null;
 		try {
 			// Determine which keys are being removed
@@ -2342,10 +2372,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			// Delete any keys that are being removed
 			if (!removed.isEmpty()) {
-				String sql = "DELETE FROM messageMetadata"
-						+ " WHERE messageId = ? AND key = ?";
+				String sql = "DELETE FROM " + tableName
+						+ " WHERE " + columnName + " = ? AND key = ?";
 				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, m.getBytes());
+				ps.setBytes(1, id);
 				for (String key : removed) {
 					ps.setString(2, key);
 					ps.addBatch();
@@ -2361,10 +2391,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			if (retained.isEmpty()) return;
 			// Update any keys that already exist
-			String sql = "UPDATE messageMetadata SET value = ?"
-					+ " WHERE messageId = ? AND key = ?";
+			String sql = "UPDATE " + tableName + " SET value = ?"
+					+ " WHERE " + columnName + " = ? AND key = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(2, m.getBytes());
+			ps.setBytes(2, id);
 			for (Entry<String, byte[]> e : retained.entrySet()) {
 				ps.setBytes(1, e.getValue());
 				ps.setString(3, e.getKey());
@@ -2378,10 +2408,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 				if (batchAffected[i] > 1) throw new DbStateException();
 			}
 			// Insert any keys that don't already exist
-			sql = "INSERT INTO messageMetadata (messageId, key, value)"
+			sql = "INSERT INTO " + tableName
+					+ " (" + columnName + ", key, value)"
 					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, m.getBytes());
+			ps.setBytes(1, id);
 			int updateIndex = 0, inserted = 0;
 			for (Entry<String, byte[]> e : retained.entrySet()) {
 				if (batchAffected[updateIndex] == 0) {
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 49502aa043ee0be44d7448565d415023fc2e4089..49c4a357c813ffccb24f2ffcbc42a9832577cb82 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -83,8 +83,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			db.addGroup(g);
 			db.addContactGroup(c, g);
 			db.setVisibility(g.getId(), Collections.singletonList(c));
+			// Attach the contact ID to the group
+			BdfDictionary d = new BdfDictionary();
+			d.put("contactId", c.getInt());
+			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
 		}
 	}
 
@@ -141,18 +147,20 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			Metadata meta = metadataEncoder.encode(d);
 			db.addLocalMessage(m.getMessage(), CLIENT_ID, meta);
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			throw new RuntimeException(e);
 		}
 	}
 
 	@Override
 	public ContactId getContactId(GroupId g) throws DbException {
-		// TODO: Use metadata to attach the contact ID to the group
-		for (Contact c : db.getContacts()) {
-			Group conversation = getConversationGroup(c);
-			if (conversation.getId().equals(g)) return c.getId();
+		try {
+			BdfDictionary d = metadataParser.parse(db.getGroupMetadata(g));
+			long id = d.getInteger("contactId");
+			return new ContactId((int) id);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			throw new NoSuchContactException();
 		}
-		throw new NoSuchContactException();
 	}
 
 	@Override
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index b4ff178a566555b8deee33b651d16d7233c7a507..4aa204bdd731af30a7529631b4a790bd235402bf 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -533,11 +533,11 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the subscription is in the DB (which it's not)
-			exactly(5).of(database).startTransaction();
+			exactly(7).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(5).of(database).containsGroup(txn, groupId);
+			exactly(7).of(database).containsGroup(txn, groupId);
 			will(returnValue(false));
-			exactly(5).of(database).abortTransaction(txn);
+			exactly(7).of(database).abortTransaction(txn);
 			// This is needed for getMessageStatus() to proceed
 			exactly(1).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -552,6 +552,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.getGroupMetadata(groupId);
+			fail();
+		} catch (NoSuchSubscriptionException expected) {
+			// Expected
+		}
+
 		try {
 			db.getMessageStatus(contactId, groupId);
 			fail();
@@ -566,6 +573,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.mergeGroupMetadata(groupId, metadata);
+			fail();
+		} catch (NoSuchSubscriptionException expected) {
+			// Expected
+		}
+
 		try {
 			db.removeGroup(group);
 			fail();
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index c0cde9e40dab1358eef222487e6327dd743ae689..d6ad4240f83c2cded1a3d866497525c569c2696e 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -43,6 +43,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.api.db.Metadata.REMOVE;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
@@ -1031,6 +1032,44 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGroupMetadata() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group
+		db.addGroup(txn, group);
+
+		// Attach some metadata to the group
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeGroupMetadata(txn, groupId, metadata);
+
+		// Retrieve the metadata for the group
+		Metadata retrieved = db.getGroupMetadata(txn, groupId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Update the metadata
+		metadata.put("foo", REMOVE);
+		metadata.put("baz", new byte[] {'q', 'u', 'x'});
+		db.mergeGroupMetadata(txn, groupId, metadata);
+
+		// Retrieve the metadata again
+		retrieved = db.getGroupMetadata(txn, groupId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testMessageMetadata() throws Exception {
 		Database<Connection> db = open(false);
@@ -1043,22 +1082,49 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Attach some metadata to the message
 		Metadata metadata = new Metadata();
 		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
 		db.mergeMessageMetadata(txn, messageId, metadata);
 
 		// Retrieve the metadata for the message
 		Metadata retrieved = db.getMessageMetadata(txn, messageId);
-		assertEquals(1, retrieved.size());
+		assertEquals(2, retrieved.size());
 		assertTrue(retrieved.containsKey("foo"));
 		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
 
 		// Retrieve the metadata for the group
 		Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId);
 		assertEquals(1, all.size());
 		assertTrue(all.containsKey(messageId));
 		retrieved = all.get(messageId);
-		assertEquals(1, retrieved.size());
+		assertEquals(2, retrieved.size());
 		assertTrue(retrieved.containsKey("foo"));
 		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Update the metadata
+		metadata.put("foo", REMOVE);
+		metadata.put("baz", new byte[] {'q', 'u', 'x'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+
+		// Retrieve the metadata again
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Retrieve the metadata for the group again
+		all = db.getMessageMetadata(txn, groupId);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId));
+		retrieved = all.get(messageId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
 
 		db.commitTransaction(txn);
 		db.close();