summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore17
-rw-r--r--MANIFEST.in14
-rw-r--r--README.rst9
-rw-r--r--UPGRADE.rst4
-rwxr-xr-xcontrib/cmdclient/console.py (renamed from cmdclient/console.py)0
-rw-r--r--contrib/cmdclient/http.py (renamed from cmdclient/http.py)0
-rw-r--r--contrib/experiments/cursesio.py (renamed from experiments/cursesio.py)0
-rw-r--r--contrib/experiments/test_messaging.py (renamed from experiments/test_messaging.py)0
-rw-r--r--contrib/graph/graph.py (renamed from graph/graph.py)0
-rw-r--r--contrib/graph/graph2.py (renamed from graph/graph2.py)30
-rw-r--r--contrib/jitsimeetbridge/jitsimeetbridge.py410
-rwxr-xr-xdemo/start.sh3
-rw-r--r--jsfiddles/create_room_send_msg/demo.css17
-rw-r--r--jsfiddles/create_room_send_msg/demo.html30
-rw-r--r--jsfiddles/create_room_send_msg/demo.js113
-rw-r--r--jsfiddles/event_stream/demo.css17
-rw-r--r--jsfiddles/event_stream/demo.html23
-rw-r--r--jsfiddles/event_stream/demo.js145
-rw-r--r--jsfiddles/example_app/demo.css43
-rw-r--r--jsfiddles/example_app/demo.details7
-rw-r--r--jsfiddles/example_app/demo.html56
-rw-r--r--jsfiddles/example_app/demo.js327
-rw-r--r--jsfiddles/register_login/demo.css7
-rw-r--r--jsfiddles/register_login/demo.html20
-rw-r--r--jsfiddles/register_login/demo.js79
-rw-r--r--jsfiddles/room_memberships/demo.css17
-rw-r--r--jsfiddles/room_memberships/demo.html37
-rw-r--r--jsfiddles/room_memberships/demo.js141
-rwxr-xr-xscripts/copyrighter-sql.pl33
-rwxr-xr-xscripts/database-prepare-for-0.0.1.sh (renamed from database-prepare-for-0.0.1.sh)0
-rwxr-xr-xscripts/database-prepare-for-0.5.0.sh (renamed from database-prepare-for-0.5.0.sh)0
-rwxr-xr-xscripts/database-save.sh (renamed from database-save.sh)0
-rwxr-xr-xscripts/nuke-room-from-db.sh (renamed from nuke-room-from-db.sh)0
-rw-r--r--scripts/sphinx_api_docs.sh (renamed from sphinx_api_docs.sh)0
-rw-r--r--setup.cfg8
-rw-r--r--synapse/__init__.py2
-rw-r--r--synapse/api/auth.py9
-rw-r--r--synapse/api/errors.py30
-rw-r--r--synapse/api/urls.py1
-rwxr-xr-xsynapse/app/homeserver.py37
-rw-r--r--synapse/config/database.py5
-rw-r--r--synapse/config/logger.py5
-rw-r--r--synapse/config/server.py8
-rw-r--r--synapse/events/builder.py6
-rw-r--r--synapse/events/utils.py16
-rw-r--r--synapse/federation/replication.py94
-rw-r--r--synapse/federation/transport/__init__.py62
-rw-r--r--synapse/federation/transport/client.py215
-rw-r--r--synapse/federation/transport/server.py (renamed from synapse/federation/transport.py)275
-rw-r--r--synapse/handlers/_base.py9
-rw-r--r--synapse/handlers/directory.py3
-rw-r--r--synapse/handlers/events.py79
-rw-r--r--synapse/handlers/federation.py29
-rw-r--r--synapse/handlers/message.py45
-rw-r--r--synapse/handlers/presence.py28
-rw-r--r--synapse/handlers/profile.py3
-rw-r--r--synapse/handlers/room.py40
-rw-r--r--synapse/handlers/typing.py21
-rw-r--r--synapse/http/client.py19
-rw-r--r--synapse/http/matrixfederationclient.py1
-rw-r--r--synapse/http/server.py8
-rw-r--r--synapse/http/servlet.py113
-rw-r--r--synapse/notifier.py4
-rw-r--r--synapse/push/__init__.py319
-rw-r--r--synapse/push/httppusher.py145
-rw-r--r--synapse/push/pusherpool.py152
-rw-r--r--synapse/python_dependencies.py80
-rw-r--r--synapse/rest/__init__.py37
-rw-r--r--synapse/rest/base.py80
-rw-r--r--synapse/rest/client/__init__.py14
-rw-r--r--synapse/rest/client/v1/__init__.py44
-rw-r--r--synapse/rest/client/v1/admin.py (renamed from synapse/rest/admin.py)8
-rw-r--r--synapse/rest/client/v1/base.py52
-rw-r--r--synapse/rest/client/v1/directory.py (renamed from synapse/rest/directory.py)11
-rw-r--r--synapse/rest/client/v1/events.py (renamed from synapse/rest/events.py)19
-rw-r--r--synapse/rest/client/v1/initial_sync.py (renamed from synapse/rest/initial_sync.py)9
-rw-r--r--synapse/rest/client/v1/login.py (renamed from synapse/rest/login.py)8
-rw-r--r--synapse/rest/client/v1/presence.py (renamed from synapse/rest/presence.py)19
-rw-r--r--synapse/rest/client/v1/profile.py (renamed from synapse/rest/profile.py)19
-rw-r--r--synapse/rest/client/v1/push_rule.py386
-rw-r--r--synapse/rest/client/v1/pusher.py80
-rw-r--r--synapse/rest/client/v1/register.py (renamed from synapse/rest/register.py)4
-rw-r--r--synapse/rest/client/v1/room.py (renamed from synapse/rest/room.py)53
-rw-r--r--synapse/rest/client/v1/transactions.py (renamed from synapse/rest/transactions.py)0
-rw-r--r--synapse/rest/client/v1/voip.py (renamed from synapse/rest/voip.py)4
-rw-r--r--synapse/rest/client/v2_alpha/__init__.py29
-rw-r--r--synapse/rest/client/v2_alpha/_base.py38
-rw-r--r--synapse/rest/media/__init__.py (renamed from synapse/media/__init__.py)0
-rw-r--r--synapse/rest/media/v0/__init__.py (renamed from synapse/media/v0/__init__.py)0
-rw-r--r--synapse/rest/media/v0/content_repository.py (renamed from synapse/media/v0/content_repository.py)0
-rw-r--r--synapse/rest/media/v1/__init__.py (renamed from synapse/media/v1/__init__.py)6
-rw-r--r--synapse/rest/media/v1/base_resource.py (renamed from synapse/media/v1/base_resource.py)0
-rw-r--r--synapse/rest/media/v1/download_resource.py (renamed from synapse/media/v1/download_resource.py)0
-rw-r--r--synapse/rest/media/v1/filepath.py (renamed from synapse/media/v1/filepath.py)0
-rw-r--r--synapse/rest/media/v1/media_repository.py (renamed from synapse/media/v1/media_repository.py)0
-rw-r--r--synapse/rest/media/v1/thumbnail_resource.py (renamed from synapse/media/v1/thumbnail_resource.py)0
-rw-r--r--synapse/rest/media/v1/thumbnailer.py (renamed from synapse/media/v1/thumbnailer.py)2
-rw-r--r--synapse/rest/media/v1/upload_resource.py (renamed from synapse/media/v1/upload_resource.py)0
-rw-r--r--synapse/server.py43
-rw-r--r--synapse/storage/__init__.py42
-rw-r--r--synapse/storage/_base.py48
-rw-r--r--synapse/storage/push_rule.py209
-rw-r--r--synapse/storage/pusher.py173
-rw-r--r--synapse/storage/room.py7
-rw-r--r--synapse/storage/roommember.py5
-rw-r--r--synapse/storage/schema/delta/v12.sql46
-rw-r--r--synapse/storage/schema/pusher.sql46
-rw-r--r--synapse/storage/state.py6
-rw-r--r--synapse/storage/stream.py197
-rw-r--r--tests/handlers/test_directory.py7
-rw-r--r--tests/handlers/test_presence.py514
-rw-r--r--tests/handlers/test_presencelike.py9
-rw-r--r--tests/handlers/test_profile.py8
-rw-r--r--tests/handlers/test_room.py13
-rw-r--r--tests/handlers/test_typing.py33
-rw-r--r--tests/rest/__init__.py3
-rw-r--r--tests/rest/client/__init__.py14
-rw-r--r--tests/rest/client/v1/__init__.py15
-rw-r--r--tests/rest/client/v1/test_events.py (renamed from tests/rest/test_events.py)14
-rw-r--r--tests/rest/client/v1/test_presence.py (renamed from tests/rest/test_presence.py)30
-rw-r--r--tests/rest/client/v1/test_profile.py (renamed from tests/rest/test_profile.py)9
-rw-r--r--tests/rest/client/v1/test_rooms.py (renamed from tests/rest/test_rooms.py)41
-rw-r--r--tests/rest/client/v1/test_typing.py (renamed from tests/rest/test_typing.py)58
-rw-r--r--tests/rest/client/v1/utils.py (renamed from tests/rest/utils.py)0
-rw-r--r--tests/rest/client/v2_alpha/__init__.py60
-rw-r--r--tests/storage/TESTS_NEEDED_FOR5
-rw-r--r--tests/storage/test_directory.py5
-rw-r--r--tests/storage/test_presence.py5
-rw-r--r--tests/storage/test_profile.py3
-rw-r--r--tests/storage/test_redaction.py7
-rw-r--r--tests/storage/test_room.py20
-rw-r--r--tests/storage/test_roommember.py9
-rw-r--r--tests/storage/test_stream.py9
-rw-r--r--tests/test_types.py12
-rw-r--r--tests/unittest.py2
-rw-r--r--tests/utils.py20
136 files changed, 3602 insertions, 2537 deletions
diff --git a/.gitignore b/.gitignore
index af90668c89..3899c5a250 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,17 +26,18 @@ htmlcov
 
 demo/*.db
 demo/*.log
+demo/*.log.*
 demo/*.pid
+demo/media_store.*
 demo/etc
 
-graph/*.svg
-graph/*.png
-graph/*.dot
-
-**/webclient/config.js
-**/webclient/test/coverage/
-**/webclient/test/environment-protractor.js
-
 uploads
 
 .idea/
+media_store/
+
+*.tac
+
+build/
+
+localhost-800*/
diff --git a/MANIFEST.in b/MANIFEST.in
index a1a77ff540..8243a942ee 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,14 @@
+include synctl
+include LICENSE
+include VERSION
+include *.rst
+include demo/README
+
+recursive-include synapse/storage/schema *.sql
+
+recursive-include demo *.dh
+recursive-include demo *.py
+recursive-include demo *.sh
 recursive-include docs *
+recursive-include scripts *
 recursive-include tests *.py
-recursive-include synapse/storage/schema *.sql
-recursive-include syweb/webclient *
diff --git a/README.rst b/README.rst
index 92b94bcd7d..768da3df64 100644
--- a/README.rst
+++ b/README.rst
@@ -108,6 +108,15 @@ To install the synapse homeserver run::
 This installs synapse, along with the libraries it uses, into
 ``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX.
 
+Your python may not give priority to locally installed libraries over system
+libraries, in which case you must add your local packages to your python path::
+
+    $ # on Linux:
+    $ export PYTHONPATH=$HOME/.local/lib/python2.7/site-packages:$PYTHONPATH
+
+    $ # on OSX:
+    $ export PYTHONPATH=$HOME/Library/Python/2.7/lib/python/site-packages:$PYTHONPATH
+
 For reliable VoIP calls to be routed via this homeserver, you MUST configure
 a TURN server.  See docs/turn-howto.rst for details.
 
diff --git a/UPGRADE.rst b/UPGRADE.rst
index 9618ad2d57..0f81f3e11f 100644
--- a/UPGRADE.rst
+++ b/UPGRADE.rst
@@ -52,7 +52,7 @@ resulting conflicts during the upgrade process.
 Before running the command the homeserver should be first completely 
 shutdown. To run it, simply specify the location of the database, e.g.:
 
-  ./database-prepare-for-0.5.0.sh "homeserver.db"
+  ./scripts/database-prepare-for-0.5.0.sh "homeserver.db"
 
 Once this has successfully completed it will be safe to restart the 
 homeserver. You may notice that the homeserver takes a few seconds longer to 
@@ -147,7 +147,7 @@ rooms the home server was a member of and room alias mappings.
 Before running the command the homeserver should be first completely 
 shutdown. To run it, simply specify the location of the database, e.g.:
 
-  ./database-prepare-for-0.0.1.sh "homeserver.db"
+  ./scripts/database-prepare-for-0.0.1.sh "homeserver.db"
 
 Once this has successfully completed it will be safe to restart the 
 homeserver. You may notice that the homeserver takes a few seconds longer to 
diff --git a/cmdclient/console.py b/contrib/cmdclient/console.py
index d9c6ec6a70..d9c6ec6a70 100755
--- a/cmdclient/console.py
+++ b/contrib/cmdclient/console.py
diff --git a/cmdclient/http.py b/contrib/cmdclient/http.py
index 869f782ec1..869f782ec1 100644
--- a/cmdclient/http.py
+++ b/contrib/cmdclient/http.py
diff --git a/experiments/cursesio.py b/contrib/experiments/cursesio.py
index 95d87a1fda..95d87a1fda 100644
--- a/experiments/cursesio.py
+++ b/contrib/experiments/cursesio.py
diff --git a/experiments/test_messaging.py b/contrib/experiments/test_messaging.py
index fedf786cec..fedf786cec 100644
--- a/experiments/test_messaging.py
+++ b/contrib/experiments/test_messaging.py
diff --git a/graph/graph.py b/contrib/graph/graph.py
index b2acadcf5e..b2acadcf5e 100644
--- a/graph/graph.py
+++ b/contrib/graph/graph.py
diff --git a/graph/graph2.py b/contrib/graph/graph2.py
index b9b8a562a0..6b551d42e5 100644
--- a/graph/graph2.py
+++ b/contrib/graph/graph2.py
@@ -23,14 +23,27 @@ import argparse
 from synapse.events import FrozenEvent
 
 
-def make_graph(db_name, room_id, file_prefix):
+def make_graph(db_name, room_id, file_prefix, limit):
     conn = sqlite3.connect(db_name)
 
-    c = conn.execute(
-        "SELECT json FROM event_json where room_id = ?",
-        (room_id,)
+    sql = (
+        "SELECT json FROM event_json as j "
+        "INNER JOIN events as e ON e.event_id = j.event_id "
+        "WHERE j.room_id = ?"
     )
 
+    args = [room_id]
+
+    if limit:
+        sql += (
+            " ORDER BY topological_ordering DESC, stream_ordering DESC "
+            "LIMIT ?"
+        )
+
+        args.append(limit)
+
+    c = conn.execute(sql, args)
+
     events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()]
 
     events.sort(key=lambda e: e.depth)
@@ -128,11 +141,16 @@ if __name__ == "__main__":
     )
     parser.add_argument(
         "-p", "--prefix", dest="prefix",
-        help="String to prefix output files with"
+        help="String to prefix output files with",
+        default="graph_output"
+    )
+    parser.add_argument(
+        "-l", "--limit",
+        help="Only retrieve the last N events.",
     )
     parser.add_argument('db')
     parser.add_argument('room')
 
     args = parser.parse_args()
 
-    make_graph(args.db, args.room, args.prefix)
+    make_graph(args.db, args.room, args.prefix, args.limit)
diff --git a/contrib/jitsimeetbridge/jitsimeetbridge.py b/contrib/jitsimeetbridge/jitsimeetbridge.py
index dbc6f6ffa5..15f8e1c48b 100644
--- a/contrib/jitsimeetbridge/jitsimeetbridge.py
+++ b/contrib/jitsimeetbridge/jitsimeetbridge.py
@@ -39,43 +39,43 @@ ROOMDOMAIN="meet.jit.si"
 #ROOMDOMAIN="conference.jitsi.vuc.me"
 
 class TrivialMatrixClient:
-	def __init__(self, access_token):
-		self.token = None
-		self.access_token = access_token
-
-	def getEvent(self):
-		while True:
-			url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
-			if self.token:
-				url += "&from="+self.token
-			req = grequests.get(url)
-			resps = grequests.map([req])
-			obj = json.loads(resps[0].content)
-			print "incoming from matrix",obj
-			if 'end' not in obj:
-				continue
-			self.token = obj['end']
-			if len(obj['chunk']):
-				return obj['chunk'][0]
-
-	def joinRoom(self, roomId):
-		url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
-		print url
-		headers={ 'Content-Type': 'application/json' }
-		req = grequests.post(url, headers=headers, data='{}')
-		resps = grequests.map([req])
-		obj = json.loads(resps[0].content)
-		print "response: ",obj
-
-	def sendEvent(self, roomId, evType, event):
-		url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
-		print url
-		print json.dumps(event)
-		headers={ 'Content-Type': 'application/json' }
-		req = grequests.post(url, headers=headers, data=json.dumps(event))
-		resps = grequests.map([req])
-		obj = json.loads(resps[0].content)
-		print "response: ",obj
+    def __init__(self, access_token):
+        self.token = None
+        self.access_token = access_token
+
+    def getEvent(self):
+        while True:
+            url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
+            if self.token:
+                url += "&from="+self.token
+            req = grequests.get(url)
+            resps = grequests.map([req])
+            obj = json.loads(resps[0].content)
+            print "incoming from matrix",obj
+            if 'end' not in obj:
+                continue
+            self.token = obj['end']
+            if len(obj['chunk']):
+                return obj['chunk'][0]
+
+    def joinRoom(self, roomId):
+        url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
+        print url
+        headers={ 'Content-Type': 'application/json' }
+        req = grequests.post(url, headers=headers, data='{}')
+        resps = grequests.map([req])
+        obj = json.loads(resps[0].content)
+        print "response: ",obj
+
+    def sendEvent(self, roomId, evType, event):
+        url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
+        print url
+        print json.dumps(event)
+        headers={ 'Content-Type': 'application/json' }
+        req = grequests.post(url, headers=headers, data=json.dumps(event))
+        resps = grequests.map([req])
+        obj = json.loads(resps[0].content)
+        print "response: ",obj
 
 
 
@@ -83,178 +83,178 @@ xmppClients = {}
 
 
 def matrixLoop():
-	while True:
-		ev = matrixCli.getEvent()
-		print ev
-		if ev['type'] == 'm.room.member':
-			print 'membership event'
-			if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
-				roomId = ev['room_id']
-				print "joining room %s" % (roomId)
-				matrixCli.joinRoom(roomId)
-		elif ev['type'] == 'm.room.message':
-			if ev['room_id'] in xmppClients:
-				print "already have a bridge for that user, ignoring"
-				continue
-			print "got message, connecting"
-			xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
-			gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
-		elif ev['type'] == 'm.call.invite':
-			print "Incoming call"
-			#sdp = ev['content']['offer']['sdp']
-			#print "sdp: %s" % (sdp)
-			#xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
-			#gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
-		elif ev['type'] == 'm.call.answer':
-			print "Call answered"
-			sdp = ev['content']['answer']['sdp']
-			if ev['room_id'] not in xmppClients:
-				print "We didn't have a call for that room"
-				continue
-			# should probably check call ID too
-			xmppCli = xmppClients[ev['room_id']]
-			xmppCli.sendAnswer(sdp)
-		elif ev['type'] == 'm.call.hangup':
-			if ev['room_id'] in xmppClients:
-				xmppClients[ev['room_id']].stop()
-				del xmppClients[ev['room_id']]
-			
+    while True:
+        ev = matrixCli.getEvent()
+        print ev
+        if ev['type'] == 'm.room.member':
+            print 'membership event'
+            if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
+                roomId = ev['room_id']
+                print "joining room %s" % (roomId)
+                matrixCli.joinRoom(roomId)
+        elif ev['type'] == 'm.room.message':
+            if ev['room_id'] in xmppClients:
+                print "already have a bridge for that user, ignoring"
+                continue
+            print "got message, connecting"
+            xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+            gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+        elif ev['type'] == 'm.call.invite':
+            print "Incoming call"
+            #sdp = ev['content']['offer']['sdp']
+            #print "sdp: %s" % (sdp)
+            #xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+            #gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+        elif ev['type'] == 'm.call.answer':
+            print "Call answered"
+            sdp = ev['content']['answer']['sdp']
+            if ev['room_id'] not in xmppClients:
+                print "We didn't have a call for that room"
+                continue
+            # should probably check call ID too
+            xmppCli = xmppClients[ev['room_id']]
+            xmppCli.sendAnswer(sdp)
+        elif ev['type'] == 'm.call.hangup':
+            if ev['room_id'] in xmppClients:
+                xmppClients[ev['room_id']].stop()
+                del xmppClients[ev['room_id']]
+
 class TrivialXmppClient:
-	def __init__(self, matrixRoom, userId):
-		self.rid = 0
-		self.matrixRoom = matrixRoom
-		self.userId = userId
-		self.running = True
-
-	def stop(self):
-		self.running = False
-
-	def nextRid(self):
-		self.rid += 1
-		return '%d' % (self.rid)
-
-	def sendIq(self, xml):
-		fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
-		#print "\t>>>%s" % (fullXml)
-		return self.xmppPoke(fullXml)
-		
-	def xmppPoke(self, xml):
-		headers = {'Content-Type': 'application/xml'}
-		req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
-		resps = grequests.map([req])
-		obj = BeautifulSoup(resps[0].content)
-		return obj
-
-	def sendAnswer(self, answer):
-		print "sdp from matrix client",answer
-		p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-		jingle, out_err = p.communicate(answer)
-		jingle = jingle % {
-			'tojid': self.callfrom,
-			'action': 'session-accept',
-			'initiator': self.callfrom,
-			'responder': self.jid,
-			'sid': self.callsid
-		}
-		print "answer jingle from sdp",jingle
-		res = self.sendIq(jingle)
-		print "reply from answer: ",res
-		
-		self.ssrcs = {}
-		jingleSoup = BeautifulSoup(jingle)
-		for cont in jingleSoup.iq.jingle.findAll('content'):
-			if cont.description:
-				self.ssrcs[cont['name']] = cont.description['ssrc']
-		print "my ssrcs:",self.ssrcs
-
-		gevent.joinall([
- 		       gevent.spawn(self.advertiseSsrcs)
-		])
-		
-	def advertiseSsrcs(self):
+    def __init__(self, matrixRoom, userId):
+        self.rid = 0
+        self.matrixRoom = matrixRoom
+        self.userId = userId
+        self.running = True
+
+    def stop(self):
+        self.running = False
+
+    def nextRid(self):
+        self.rid += 1
+        return '%d' % (self.rid)
+
+    def sendIq(self, xml):
+        fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
+        #print "\t>>>%s" % (fullXml)
+        return self.xmppPoke(fullXml)
+
+    def xmppPoke(self, xml):
+        headers = {'Content-Type': 'application/xml'}
+        req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
+        resps = grequests.map([req])
+        obj = BeautifulSoup(resps[0].content)
+        return obj
+
+    def sendAnswer(self, answer):
+        print "sdp from matrix client",answer
+        p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+        jingle, out_err = p.communicate(answer)
+        jingle = jingle % {
+            'tojid': self.callfrom,
+            'action': 'session-accept',
+            'initiator': self.callfrom,
+            'responder': self.jid,
+            'sid': self.callsid
+        }
+        print "answer jingle from sdp",jingle
+        res = self.sendIq(jingle)
+        print "reply from answer: ",res
+
+        self.ssrcs = {}
+        jingleSoup = BeautifulSoup(jingle)
+        for cont in jingleSoup.iq.jingle.findAll('content'):
+            if cont.description:
+                self.ssrcs[cont['name']] = cont.description['ssrc']
+        print "my ssrcs:",self.ssrcs
+
+        gevent.joinall([
+                gevent.spawn(self.advertiseSsrcs)
+        ])
+
+    def advertiseSsrcs(self):
                 time.sleep(7)
-		print "SSRC spammer started"
-		while self.running:
-			ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
-			res = self.sendIq(ssrcMsg)
-			print "reply from ssrc announce: ",res
-			time.sleep(10)
-		
-		
-
-	def xmppLoop(self):
-		self.matrixCallId = time.time()
-		res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), HOST))
-
-		print res
-		self.sid = res.body['sid']
-		print "sid %s" % (self.sid)
-
-		res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
-
-		res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), self.sid, HOST))
-	
-		res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
-		print res
-
-		self.jid = res.body.iq.bind.jid.string
-		print "jid: %s" % (self.jid)
-		self.shortJid = self.jid.split('-')[0]
-
-		res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
-
-		#randomthing = res.body.iq['to']
-		#whatsitpart = randomthing.split('-')[0]
-
-		#print "other random bind thing: %s" % (randomthing)
-
-		# advertise preence to the jitsi room, with our nick
-		res = self.sendIq("<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
-		self.muc = {'users': []}
-		for p in res.body.findAll('presence'):
-			u = {}
-			u['shortJid'] = p['from'].split('/')[1]
-			if p.c and p.c.nick:
-				u['nick'] = p.c.nick.string
-			self.muc['users'].append(u)
-		print "muc: ",self.muc
-
-		# wait for stuff
-		while True:
-			print "waiting..."
-			res = self.sendIq("")
-			print "got from stream: ",res
-			if res.body.iq:
-				jingles = res.body.iq.findAll('jingle')
-				if len(jingles):
-					self.callfrom = res.body.iq['from']
-					self.handleInvite(jingles[0])
-			elif 'type' in res.body and res.body['type'] == 'terminate':
-				self.running = False
-				del xmppClients[self.matrixRoom]
-            			return 
-
-	def handleInvite(self, jingle):
-		self.initiator = jingle['initiator']
-		self.callsid = jingle['sid']
-		p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-		print "raw jingle invite",str(jingle)
-		sdp, out_err = p.communicate(str(jingle))
-		print "transformed remote offer sdp",sdp
-		inviteEvent = {
-			'offer': {
-				'type': 'offer',
-				'sdp': sdp
-			},
-			'call_id': self.matrixCallId,
-			'version': 0,
-			'lifetime': 30000
-		}
-		matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
-		
+        print "SSRC spammer started"
+        while self.running:
+            ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
+            res = self.sendIq(ssrcMsg)
+            print "reply from ssrc announce: ",res
+            time.sleep(10)
+
+
+
+    def xmppLoop(self):
+        self.matrixCallId = time.time()
+        res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), HOST))
+
+        print res
+        self.sid = res.body['sid']
+        print "sid %s" % (self.sid)
+
+        res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
+
+        res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), self.sid, HOST))
+
+        res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
+        print res
+
+        self.jid = res.body.iq.bind.jid.string
+        print "jid: %s" % (self.jid)
+        self.shortJid = self.jid.split('-')[0]
+
+        res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
+
+        #randomthing = res.body.iq['to']
+        #whatsitpart = randomthing.split('-')[0]
+
+        #print "other random bind thing: %s" % (randomthing)
+
+        # advertise preence to the jitsi room, with our nick
+        res = self.sendIq("<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
+        self.muc = {'users': []}
+        for p in res.body.findAll('presence'):
+            u = {}
+            u['shortJid'] = p['from'].split('/')[1]
+            if p.c and p.c.nick:
+                u['nick'] = p.c.nick.string
+            self.muc['users'].append(u)
+        print "muc: ",self.muc
+
+        # wait for stuff
+        while True:
+            print "waiting..."
+            res = self.sendIq("")
+            print "got from stream: ",res
+            if res.body.iq:
+                jingles = res.body.iq.findAll('jingle')
+                if len(jingles):
+                    self.callfrom = res.body.iq['from']
+                    self.handleInvite(jingles[0])
+            elif 'type' in res.body and res.body['type'] == 'terminate':
+                self.running = False
+                del xmppClients[self.matrixRoom]
+                        return
+
+    def handleInvite(self, jingle):
+        self.initiator = jingle['initiator']
+        self.callsid = jingle['sid']
+        p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+        print "raw jingle invite",str(jingle)
+        sdp, out_err = p.communicate(str(jingle))
+        print "transformed remote offer sdp",sdp
+        inviteEvent = {
+            'offer': {
+                'type': 'offer',
+                'sdp': sdp
+            },
+            'call_id': self.matrixCallId,
+            'version': 0,
+            'lifetime': 30000
+        }
+        matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
+
 matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
 
 gevent.joinall([
-	gevent.spawn(matrixLoop)
+    gevent.spawn(matrixLoop)
 ])
 
diff --git a/demo/start.sh b/demo/start.sh
index ce3e292486..bb2248770d 100755
--- a/demo/start.sh
+++ b/demo/start.sh
@@ -32,7 +32,8 @@ for port in 8080 8081 8082; do
         -D --pid-file "$DIR/$port.pid" \
         --manhole $((port + 1000)) \
         --tls-dh-params-path "demo/demo.tls.dh" \
-		$PARAMS $SYNAPSE_PARAMS
+        --media-store-path "demo/media_store.$port" \
+		$PARAMS $SYNAPSE_PARAMS \
 
     python -m synapse.app.homeserver \
         --config-path "demo/etc/$port.config" \
diff --git a/jsfiddles/create_room_send_msg/demo.css b/jsfiddles/create_room_send_msg/demo.css
deleted file mode 100644
index 48a55f372d..0000000000
--- a/jsfiddles/create_room_send_msg/demo.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.loggedin {
-    visibility: hidden;
-}
-
-p {
-    font-family: monospace;
-}
-
-table
-{
-    border-spacing:5px;
-}
-
-th,td
-{
-    padding:5px;
-}
diff --git a/jsfiddles/create_room_send_msg/demo.html b/jsfiddles/create_room_send_msg/demo.html
deleted file mode 100644
index 088ff7ac0f..0000000000
--- a/jsfiddles/create_room_send_msg/demo.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<div>
-    <p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
-</div>
-<form class="loginForm">
-    <input type="text" id="userLogin" placeholder="Username"></input>
-    <input type="password" id="passwordLogin" placeholder="Password"></input>
-    <input type="button" class="login" value="Login"></input>
-</form>
-<div class="loggedin">
-    <form class="createRoomForm">
-        <input type="text" id="roomAlias" placeholder="Room alias (optional)"></input>
-        <input type="button" class="createRoom" value="Create Room"></input>
-    </form>
-    <form class="sendMessageForm">
-        <input type="text" id="roomId" placeholder="Room ID"></input>
-        <input type="text" id="messageBody" placeholder="Message body"></input>
-        <input type="button" class="sendMessage" value="Send Message"></input>
-    </form>
-    <table id="rooms">
-        <tbody>
-            <tr>
-                <th>Room ID</th>
-                <th>My state</th>
-                <th>Room Alias</th>
-                <th>Latest message</th>
-            </tr>
-        </tbody>
-    </table>
-</div>
-    
diff --git a/jsfiddles/create_room_send_msg/demo.js b/jsfiddles/create_room_send_msg/demo.js
deleted file mode 100644
index 9c346e2f64..0000000000
--- a/jsfiddles/create_room_send_msg/demo.js
+++ /dev/null
@@ -1,113 +0,0 @@
-var accountInfo = {};
-
-var showLoggedIn = function(data) {
-    accountInfo = data;
-    getCurrentRoomList();
-    $(".loggedin").css({visibility: "visible"});
-};
-
-$('.login').live('click', function() {
-    var user = $("#userLogin").val();
-    var password = $("#passwordLogin").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/login",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            showLoggedIn(data);
-        },
-        error: function(err) {
-            var errMsg = "To try this, you need a home server running!";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson) {
-                errMsg = JSON.stringify(errJson);   
-            }
-            alert(errMsg);  
-        }
-    }); 
-});
-
-var getCurrentRoomList = function() {
-    var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
-    $.getJSON(url, function(data) {
-        var rooms = data.rooms;
-        for (var i=0; i<rooms.length; ++i) {
-            rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
-            addRoom(rooms[i]);   
-        }
-    }).fail(function(err) {
-        alert(JSON.stringify($.parseJSON(err.responseText)));
-    });
-};
-
-$('.createRoom').live('click', function() {
-    var roomAlias = $("#roomAlias").val();
-    var data = {};
-    if (roomAlias.length > 0) {
-        data.room_alias_name = roomAlias;   
-    }
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            data.membership = "join"; // you are automatically joined into every room you make.
-            data.latest_message = "";
-            addRoom(data);
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
-
-var addRoom = function(data) {
-    row = "<tr>" +
-        "<td>"+data.room_id+"</td>" +
-        "<td>"+data.membership+"</td>" +
-        "<td>"+data.room_alias+"</td>" +
-        "<td>"+data.latest_message+"</td>" +
-        "</tr>";
-    $("#rooms").append(row);
-};
-
-$('.sendMessage').live('click', function() {
-    var roomId = $("#roomId").val();
-    var body = $("#messageBody").val();
-    var msgId = $.now();
-    
-    if (roomId.length === 0 || body.length === 0) {
-        return;
-    }
-    
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$roomid", encodeURIComponent(roomId));
-    
-    var data = {
-        msgtype: "m.text",
-        body: body
-    };
-    
-    $.ajax({
-        url: url,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            $("#messageBody").val("");
-            // wipe the table and reload it. Using the event stream would be the best
-            // solution but that is out of scope of this fiddle.
-            $("#rooms").find("tr:gt(0)").remove();
-            getCurrentRoomList();
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
diff --git a/jsfiddles/event_stream/demo.css b/jsfiddles/event_stream/demo.css
deleted file mode 100644
index 48a55f372d..0000000000
--- a/jsfiddles/event_stream/demo.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.loggedin {
-    visibility: hidden;
-}
-
-p {
-    font-family: monospace;
-}
-
-table
-{
-    border-spacing:5px;
-}
-
-th,td
-{
-    padding:5px;
-}
diff --git a/jsfiddles/event_stream/demo.html b/jsfiddles/event_stream/demo.html
deleted file mode 100644
index 7657780d28..0000000000
--- a/jsfiddles/event_stream/demo.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<div>
-    <p>This event stream demo requires a home server to be running on http://localhost:8008</p>
-</div>
-<form class="loginForm">
-    <input type="text" id="userLogin" placeholder="Username"></input>
-    <input type="password" id="passwordLogin" placeholder="Password"></input>
-    <input type="button" class="login" value="Login"></input>
-</form>
-<div class="loggedin">
-    <form class="sendMessageForm">
-        <input type="button" class="sendMessage" value="Send random message"></input>
-    </form>
-    <p id="streamErrorText"></p>
-    <table id="rooms">
-        <tbody>
-            <tr>
-                <th>Room ID</th>
-                <th>Latest message</th>
-            </tr>
-        </tbody>
-    </table>
-</div>
-    
diff --git a/jsfiddles/event_stream/demo.js b/jsfiddles/event_stream/demo.js
deleted file mode 100644
index acba8391fa..0000000000
--- a/jsfiddles/event_stream/demo.js
+++ /dev/null
@@ -1,145 +0,0 @@
-var accountInfo = {};
-
-var eventStreamInfo = {
-    from: "END"
-};
-
-var roomInfo = [];
-
-var longpollEventStream = function() {
-    var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$from", eventStreamInfo.from);
-    
-    $.getJSON(url, function(data) {
-        eventStreamInfo.from = data.end;
-        
-        var hasNewLatestMessage = false;
-        for (var i=0; i<data.chunk.length; ++i) {
-            if (data.chunk[i].type === "m.room.message") {
-                for (var j=0; j<roomInfo.length; ++j) {
-                    if (roomInfo[j].room_id === data.chunk[i].room_id) {
-                        roomInfo[j].latest_message = data.chunk[i].content.body;
-                        hasNewLatestMessage = true;
-                    }
-                }
-            }
-        }
-        
-        if (hasNewLatestMessage) {
-           setRooms(roomInfo);
-        }
-        $("#streamErrorText").text("");
-        longpollEventStream();
-    }).fail(function(err) {
-        $("#streamErrorText").text("Event stream error: "+JSON.stringify($.parseJSON(err.responseText)));
-        setTimeout(longpollEventStream, 5000);
-    });
-};
-
-var showLoggedIn = function(data) {
-    accountInfo = data;
-    longpollEventStream();
-    getCurrentRoomList();
-    $(".loggedin").css({visibility: "visible"});
-};
-
-$('.login').live('click', function() {
-    var user = $("#userLogin").val();
-    var password = $("#passwordLogin").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/login",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            $("#rooms").find("tr:gt(0)").remove();
-            showLoggedIn(data);
-        },
-        error: function(err) {
-            var errMsg = "To try this, you need a home server running!";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson) {
-                errMsg = JSON.stringify(errJson);   
-            }
-            alert(errMsg);  
-        }
-    }); 
-});
-
-var getCurrentRoomList = function() {
-    $("#roomId").val("");
-    var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
-    $.getJSON(url, function(data) {
-        var rooms = data.rooms;
-        for (var i=0; i<rooms.length; ++i) {
-            if ("messages" in rooms[i]) {
-                rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;   
-            }
-        }
-        roomInfo = rooms;
-        setRooms(roomInfo);  
-    }).fail(function(err) {
-        alert(JSON.stringify($.parseJSON(err.responseText)));
-    });
-};
-
-$('.sendMessage').live('click', function() {
-    if (roomInfo.length === 0) {
-        alert("There is no room to send a message to!");
-        return;
-    }
-    
-    var index = Math.floor(Math.random() * roomInfo.length);
-    
-    sendMessage(roomInfo[index].room_id);
-});
-
-var sendMessage = function(roomId) {
-    var body = "jsfiddle message @" + $.now();
-    
-    if (roomId.length === 0) {
-        return;
-    }
-    
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$roomid", encodeURIComponent(roomId));
-    
-    var data = {
-        msgtype: "m.text",
-        body: body
-    };
-    
-    $.ajax({
-        url: url,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            $("#messageBody").val("");
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-};
-
-var setRooms = function(roomList) {
-    // wipe existing entries
-    $("#rooms").find("tr:gt(0)").remove();
-    
-    var rows = "";
-    for (var i=0; i<roomList.length; ++i) {
-        row = "<tr>" +
-            "<td>"+roomList[i].room_id+"</td>" +
-            "<td>"+roomList[i].latest_message+"</td>" +
-            "</tr>";  
-        rows += row;
-    }
-    
-    $("#rooms").append(rows);
-};
-
diff --git a/jsfiddles/example_app/demo.css b/jsfiddles/example_app/demo.css
deleted file mode 100644
index 4c1e157cc8..0000000000
--- a/jsfiddles/example_app/demo.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.roomListDashboard, .roomContents, .sendMessageForm {
-    visibility: hidden;
-}
-
-.roomList {
-    background-color: #909090;
-}
-
-.messageWrapper {
-    background-color: #EEEEEE;
-    height: 400px;
-    overflow: scroll;
-}
-
-.membersWrapper {
-    background-color: #EEEEEE;
-    height: 200px;
-    width: 50%;
-    overflow: scroll;
-}
-
-.textEntry {
-    width: 100%
-}
-
-p {
-    font-family: monospace;
-}
-
-table
-{
-    border-spacing:5px;
-}
-
-th,td
-{
-    padding:5px;
-}
-
-.roomList tr:not(:first-child):hover {
-    background-color: orange;
-    cursor: pointer;
-}
diff --git a/jsfiddles/example_app/demo.details b/jsfiddles/example_app/demo.details
deleted file mode 100644
index 3f96d3e744..0000000000
--- a/jsfiddles/example_app/demo.details
+++ /dev/null
@@ -1,7 +0,0 @@
- name: Example Matrix Client
- description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
- authors:
-   - matrix.org
- resources:
-   - http://matrix.org
- normalize_css: no
\ No newline at end of file
diff --git a/jsfiddles/example_app/demo.html b/jsfiddles/example_app/demo.html
deleted file mode 100644
index 7a9dffddd0..0000000000
--- a/jsfiddles/example_app/demo.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<div class="signUp">
-    <p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
-    <form class="registrationForm">
-        <p>No account? Register:</p>
-        <input type="text" id="userReg" placeholder="Username"></input>
-        <input type="password" id="passwordReg" placeholder="Password"></input>
-        <input type="button" class="register" value="Register"></input>
-    </form>
-    <form class="loginForm">
-        <p>Got an account? Login:</p>
-        <input type="text" id="userLogin" placeholder="Username"></input>
-        <input type="password" id="passwordLogin" placeholder="Password"></input>
-        <input type="button" class="login" value="Login"></input>
-    </form>
-</div>
-
-<div class="roomListDashboard">
-    <form class="createRoomForm">
-        <input type="text" id="roomAlias" placeholder="Room alias"></input>
-        <input type="button" class="createRoom" value="Create Room"></input>
-    </form>
-    <table id="rooms" class="roomList">
-        <tbody>
-            <tr>
-                <th>Room</th>
-                <th>My state</th>
-                <th>Latest message</th>
-            </tr>
-        </tbody>
-    </table>
-</div>
-
-<div class="roomContents">
-    <p id="roomName">Select a room</p>
-    <div class="messageWrapper">
-        <table id="messages">
-            <tbody>
-            </tbody>
-        </table>
-    </div>
-    <form class="sendMessageForm">
-        <input type="text" class="textEntry" id="body" placeholder="Enter text here..." onkeydown="javascript:if (event.keyCode == 13) document.getElementById('sendMsg').focus()"></input>
-        <input type="button" class="sendMessage" id="sendMsg" value="Send"></input>
-    </form>
-</div>
-
-<div>
-    <p>Member list:</p>
-    <div class="membersWrapper">
-        <table id="members">
-            <tbody>
-            </tbody>
-        </table>
-    </div>
-</div>
-    
diff --git a/jsfiddles/example_app/demo.js b/jsfiddles/example_app/demo.js
deleted file mode 100644
index 13c9c2b339..0000000000
--- a/jsfiddles/example_app/demo.js
+++ /dev/null
@@ -1,327 +0,0 @@
-var accountInfo = {};
-
-var eventStreamInfo = {
-    from: "END"
-};
-
-var roomInfo = [];
-var memberInfo = [];
-var viewingRoomId;
-
-// ************** Event Streaming **************
-var longpollEventStream = function() {
-    var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$from", eventStreamInfo.from);
-
-    $.getJSON(url, function(data) {
-        eventStreamInfo.from = data.end;
-        
-        var hasNewLatestMessage = false;
-        var updatedMemberList = false;
-        var i=0;
-        var j=0;
-        for (i=0; i<data.chunk.length; ++i) {
-            if (data.chunk[i].type === "m.room.message") {
-                console.log("Got new message: " + JSON.stringify(data.chunk[i]));
-                if (viewingRoomId === data.chunk[i].room_id) {
-                    addMessage(data.chunk[i]);
-                }
-                
-                for (j=0; j<roomInfo.length; ++j) {
-                    if (roomInfo[j].room_id === data.chunk[i].room_id) {
-                        roomInfo[j].latest_message = data.chunk[i].content.body;
-                        hasNewLatestMessage = true;
-                    }
-                }
-            }
-            else if (data.chunk[i].type === "m.room.member") {
-                if (viewingRoomId === data.chunk[i].room_id) {
-                    console.log("Got new member: " + JSON.stringify(data.chunk[i]));
-                    addMessage(data.chunk[i]);
-                    for (j=0; j<memberInfo.length; ++j) {
-                        if (memberInfo[j].state_key === data.chunk[i].state_key) {
-                            memberInfo[j] = data.chunk[i];
-                            updatedMemberList = true;
-                            break;
-                        }
-                    }
-                    if (!updatedMemberList) {
-                        memberInfo.push(data.chunk[i]);  
-                        updatedMemberList = true;
-                    }
-                }
-                if (data.chunk[i].state_key === accountInfo.user_id) {
-                    getCurrentRoomList(); // update our join/invite list
-                }
-            }
-            else {
-                console.log("Discarding: " + JSON.stringify(data.chunk[i]));
-            }
-        }
-        
-        if (hasNewLatestMessage) {
-           setRooms(roomInfo);
-        }
-        if (updatedMemberList) {
-            $("#members").empty();
-            for (i=0; i<memberInfo.length; ++i) { 
-                addMember(memberInfo[i]);
-            }
-        }
-        longpollEventStream();
-    }).fail(function(err) {
-        setTimeout(longpollEventStream, 5000);
-    });
-};
-
-// ************** Registration and Login **************
-var onLoggedIn = function(data) {
-    accountInfo = data;
-    longpollEventStream();
-    getCurrentRoomList();
-    $(".roomListDashboard").css({visibility: "visible"});
-    $(".roomContents").css({visibility: "visible"});
-    $(".signUp").css({display: "none"});
-};
-
-$('.login').live('click', function() {
-    var user = $("#userLogin").val();
-    var password = $("#passwordLogin").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/login",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            onLoggedIn(data);
-        },
-        error: function(err) {
-            alert("Unable to login: is the home server running?");  
-        }
-    }); 
-});
-
-$('.register').live('click', function() {
-    var user = $("#userReg").val();
-    var password = $("#passwordReg").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/register",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            onLoggedIn(data);
-        },
-        error: function(err) {
-            var msg = "Is the home server running?";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson !== null) {
-                msg = errJson.error;   
-            }
-            alert("Unable to register: "+msg);  
-        }
-    });
-});
-
-// ************** Creating a room ******************
-$('.createRoom').live('click', function() {
-    var roomAlias = $("#roomAlias").val();
-    var data = {};
-    if (roomAlias.length > 0) {
-        data.room_alias_name = roomAlias;   
-    }
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(response) {
-            $("#roomAlias").val("");
-            response.membership = "join"; // you are automatically joined into every room you make.
-            response.latest_message = "";
-            
-            roomInfo.push(response);
-            setRooms(roomInfo);
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
-
-// ************** Getting current state **************
-var getCurrentRoomList = function() {
-    var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
-    $.getJSON(url, function(data) {
-        var rooms = data.rooms;
-        for (var i=0; i<rooms.length; ++i) {
-            if ("messages" in rooms[i]) {
-                rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;   
-            }
-        }
-        roomInfo = rooms;
-        setRooms(roomInfo);  
-    }).fail(function(err) {
-        alert(JSON.stringify($.parseJSON(err.responseText)));
-    });
-};
-
-var loadRoomContent = function(roomId) {
-    console.log("loadRoomContent " + roomId);
-    viewingRoomId = roomId;
-    $("#roomName").text("Room: "+roomId);
-    $(".sendMessageForm").css({visibility: "visible"});
-    getMessages(roomId);
-    getMemberList(roomId);
-};
-
-var getMessages = function(roomId) {
-    $("#messages").empty();
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" + 
-              encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
-    $.getJSON(url, function(data) {
-        for (var i=data.chunk.length-1; i>=0; --i) {
-            addMessage(data.chunk[i]);   
-        }
-    });
-};
-
-var getMemberList = function(roomId) {
-    $("#members").empty();
-    memberInfo = [];
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" + 
-              encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
-    $.getJSON(url, function(data) {
-        for (var i=0; i<data.chunk.length; ++i) {
-            memberInfo.push(data.chunk[i]);
-            addMember(data.chunk[i]);   
-        }
-    });
-};
-
-// ************** Sending messages **************
-$('.sendMessage').live('click', function() {
-    if (viewingRoomId === undefined) {
-        alert("There is no room to send a message to!");
-        return;
-    }
-    var body = $("#body").val();
-    sendMessage(viewingRoomId, body);
-});
-
-var sendMessage = function(roomId, body) {
-    var msgId = $.now();
-    
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$roomid", encodeURIComponent(roomId));
-    
-    var data = {
-        msgtype: "m.text",
-        body: body
-    };
-    
-    $.ajax({
-        url: url,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            $("#body").val("");
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    });
-};
-
-// ************** Navigation and DOM manipulation **************
-var setRooms = function(roomList) {
-    // wipe existing entries
-    $("#rooms").find("tr:gt(0)").remove();
-    
-    var rows = "";
-    for (var i=0; i<roomList.length; ++i) {
-        row = "<tr>" +
-              "<td>"+roomList[i].room_id+"</td>" +
-              "<td>"+roomList[i].membership+"</td>" +
-              "<td>"+roomList[i].latest_message+"</td>" +
-              "</tr>";  
-        rows += row;
-    }
-    
-    $("#rooms").append(rows);
-    
-    $('#rooms').find("tr").click(function(){
-        var roomId = $(this).find('td:eq(0)').text();
-        var membership = $(this).find('td:eq(1)').text();
-        if (membership !== "join") {
-            console.log("Joining room " + roomId); 
-            var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
-            url = url.replace("$token", accountInfo.access_token);
-            url = url.replace("$roomid", encodeURIComponent(roomId));
-            $.ajax({
-                url: url,
-                type: "POST",
-                contentType: "application/json; charset=utf-8",
-                data: JSON.stringify({membership: "join"}),
-                dataType: "json",
-                success: function(data) {
-                    loadRoomContent(roomId);
-                    getCurrentRoomList();
-                },
-                error: function(err) {
-                    alert(JSON.stringify($.parseJSON(err.responseText)));  
-                }
-            });
-        }
-        else {
-            loadRoomContent(roomId);
-        }
-    });
-};
-
-var addMessage = function(data) {
-
-    var msg = data.content.body;
-    if (data.type === "m.room.member") {
-        if (data.content.membership === undefined) {
-            return;
-        }
-        if (data.content.membership === "invite") {
-            msg = "<em>invited " + data.state_key + " to the room</em>";
-        }
-        else if (data.content.membership === "join") {
-            msg = "<em>joined the room</em>";
-        }
-        else if (data.content.membership === "leave") {
-            msg = "<em>left the room</em>";
-        }
-        else if (data.content.membership === "ban") {
-            msg = "<em>was banned from the room</em>";
-        }
-    }
-    if (msg === undefined) {
-        return;
-    }
-
-    var row = "<tr>" +
-              "<td>"+data.user_id+"</td>" +
-              "<td>"+msg+"</td>" +
-              "</tr>"; 
-    $("#messages").append(row);
-};
-
-var addMember = function(data) {
-    var row = "<tr>" +
-              "<td>"+data.state_key+"</td>" +
-              "<td>"+data.content.membership+"</td>" +
-              "</tr>"; 
-    $("#members").append(row);
-};
-
diff --git a/jsfiddles/register_login/demo.css b/jsfiddles/register_login/demo.css
deleted file mode 100644
index 11781c250f..0000000000
--- a/jsfiddles/register_login/demo.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.loggedin {
-    visibility: hidden;
-}
-
-p {
-    font-family: monospace;
-}
diff --git a/jsfiddles/register_login/demo.html b/jsfiddles/register_login/demo.html
deleted file mode 100644
index fcac453ac2..0000000000
--- a/jsfiddles/register_login/demo.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<div>
-    <p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
-</div>
-<form class="registrationForm">
-    <input type="text" id="user" placeholder="Username"></input>
-    <input type="password" id="password" placeholder="Password"></input>
-    <input type="button" class="register" value="Register"></input>
-</form>
-<form class="loginForm">
-    <input type="text" id="userLogin" placeholder="Username"></input>
-    <input type="password" id="passwordLogin" placeholder="Password"></input>
-    <input type="button" class="login" value="Login"></input>
-</form>
-<div class="loggedin">
-    <p id="welcomeText"></p>
-    <input type="button" class="testToken" value="Test token"></input>
-    <input type="button" class="logout" value="Logout"></input>
-    <p id="imSyncText"></p>
-</div>
-    
diff --git a/jsfiddles/register_login/demo.js b/jsfiddles/register_login/demo.js
deleted file mode 100644
index 2e6957b631..0000000000
--- a/jsfiddles/register_login/demo.js
+++ /dev/null
@@ -1,79 +0,0 @@
-var accountInfo = {};
-
-var showLoggedIn = function(data) {
-    accountInfo = data;
-    $(".loggedin").css({visibility: "visible"});
-    $("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is:  " +
-                           accountInfo.access_token);    
-};
-
-$('.register').live('click', function() {
-    var user = $("#user").val();
-    var password = $("#password").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/register",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            showLoggedIn(data);
-        },
-        error: function(err) {
-            var errMsg = "To try this, you need a home server running!";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson) {
-                errMsg = JSON.stringify(errJson);   
-            }
-            alert(errMsg);   
-        }
-    });
-});
-
-var login = function(user, password) {
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/login",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            showLoggedIn(data);
-        },
-        error: function(err) {
-            var errMsg = "To try this, you need a home server running!";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson) {
-                errMsg = JSON.stringify(errJson);   
-            }
-            alert(errMsg);  
-        }
-    });  
-};
-
-$('.login').live('click', function() {
-    var user = $("#userLogin").val();
-    var password = $("#passwordLogin").val();
-    $.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) {
-        if (data.flows[0].type !== "m.login.password") {
-            alert("I don't know how to login with this type: " + data.type);
-            return;
-        }
-        login(user, password);
-    });
-});
-
-$('.logout').live('click', function() {
-    accountInfo = {};
-    $("#imSyncText").text("");
-    $(".loggedin").css({visibility: "hidden"});
-});
-
-$('.testToken').live('click', function() {
-    var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
-    $.getJSON(url, function(data) {
-         $("#imSyncText").text(JSON.stringify(data, undefined, 2));
-    }).fail(function(err) {
-        $("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText)));
-    });
-});
diff --git a/jsfiddles/room_memberships/demo.css b/jsfiddles/room_memberships/demo.css
deleted file mode 100644
index 48a55f372d..0000000000
--- a/jsfiddles/room_memberships/demo.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.loggedin {
-    visibility: hidden;
-}
-
-p {
-    font-family: monospace;
-}
-
-table
-{
-    border-spacing:5px;
-}
-
-th,td
-{
-    padding:5px;
-}
diff --git a/jsfiddles/room_memberships/demo.html b/jsfiddles/room_memberships/demo.html
deleted file mode 100644
index e6f39df5aa..0000000000
--- a/jsfiddles/room_memberships/demo.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<div>
-    <p>This room membership demo requires a home server to be running on http://localhost:8008</p>
-</div>
-<form class="loginForm">
-    <input type="text" id="userLogin" placeholder="Username"></input>
-    <input type="password" id="passwordLogin" placeholder="Password"></input>
-    <input type="button" class="login" value="Login"></input>
-</form>
-<div class="loggedin">
-    <form class="createRoomForm">
-        <input type="button" class="createRoom" value="Create Room"></input>
-    </form>
-    <form class="changeMembershipForm">
-        <input type="text" id="roomId" placeholder="Room ID"></input>
-        <input type="text" id="targetUser" placeholder="Target User ID"></input>
-        <select id="membership">
-            <option value="invite">invite</option>
-            <option value="join">join</option>
-            <option value="leave">leave</option>
-        </select>
-        <input type="button" class="changeMembership" value="Change Membership"></input>
-    </form>
-    <form class="joinAliasForm">
-        <input type="text" id="roomAlias" placeholder="Room Alias (#name:domain)"></input>
-        <input type="button" class="joinAlias" value="Join via Alias"></input>
-    </form>
-    <table id="rooms">
-        <tbody>
-            <tr>
-                <th>Room ID</th>
-                <th>My state</th>
-                <th>Room Alias</th>
-            </tr>
-        </tbody>
-    </table>
-</div>
-    
diff --git a/jsfiddles/room_memberships/demo.js b/jsfiddles/room_memberships/demo.js
deleted file mode 100644
index 8a7b1aa88e..0000000000
--- a/jsfiddles/room_memberships/demo.js
+++ /dev/null
@@ -1,141 +0,0 @@
-var accountInfo = {};
-
-var showLoggedIn = function(data) {
-    accountInfo = data;
-    getCurrentRoomList();
-    $(".loggedin").css({visibility: "visible"});
-    $("#membership").change(function() {
-    if ($("#membership").val() === "invite") {
-        $("#targetUser").css({visibility: "visible"});
-    }
-    else {
-        $("#targetUser").css({visibility: "hidden"});
-    }
-});
-};
-
-$('.login').live('click', function() {
-    var user = $("#userLogin").val();
-    var password = $("#passwordLogin").val();
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/login",
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
-        dataType: "json",
-        success: function(data) {
-            $("#rooms").find("tr:gt(0)").remove();
-            showLoggedIn(data);
-        },
-        error: function(err) {
-            var errMsg = "To try this, you need a home server running!";
-            var errJson = $.parseJSON(err.responseText);
-            if (errJson) {
-                errMsg = JSON.stringify(errJson);   
-            }
-            alert(errMsg); 
-        }
-    }); 
-});
-
-var getCurrentRoomList = function() {
-    $("#roomId").val("");
-    // wipe the table and reload it. Using the event stream would be the best
-    // solution but that is out of scope of this fiddle.
-    $("#rooms").find("tr:gt(0)").remove();
-    
-    var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
-    $.getJSON(url, function(data) {
-        var rooms = data.rooms;
-        for (var i=0; i<rooms.length; ++i) {
-            addRoom(rooms[i]);   
-        }
-    }).fail(function(err) {
-        alert(JSON.stringify($.parseJSON(err.responseText)));
-    });
-};
-
-$('.createRoom').live('click', function() {
-    var data = {};
-    $.ajax({
-        url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            data.membership = "join"; // you are automatically joined into every room you make.
-            data.latest_message = "";
-            addRoom(data);
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
-
-var addRoom = function(data) {
-    row = "<tr>" +
-        "<td>"+data.room_id+"</td>" +
-        "<td>"+data.membership+"</td>" +
-        "<td>"+data.room_alias+"</td>" +
-        "</tr>";
-    $("#rooms").append(row);
-};
-
-$('.changeMembership').live('click', function() {
-    var roomId = $("#roomId").val();
-    var member = $("#targetUser").val();
-    var membership = $("#membership").val();
-    
-    if (roomId.length === 0) {
-        return;
-    }
-    
-    var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$roomid", encodeURIComponent(roomId));
-    url = url.replace("$membership", membership);
-    
-    var data = {};
-    
-    if (membership === "invite") {
-        data = {
-            user_id: member
-        };
-    }
-
-    $.ajax({
-        url: url,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify(data),
-        dataType: "json",
-        success: function(data) {
-            getCurrentRoomList();
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
-
-$('.joinAlias').live('click', function() {
-    var roomAlias = $("#roomAlias").val();
-    var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token";
-    url = url.replace("$token", accountInfo.access_token);
-    url = url.replace("$roomalias", encodeURIComponent(roomAlias));
-    $.ajax({
-        url: url,
-        type: "POST",
-        contentType: "application/json; charset=utf-8",
-        data: JSON.stringify({}),
-        dataType: "json",
-        success: function(data) {
-            getCurrentRoomList();
-        },
-        error: function(err) {
-            alert(JSON.stringify($.parseJSON(err.responseText)));  
-        }
-    }); 
-});
diff --git a/scripts/copyrighter-sql.pl b/scripts/copyrighter-sql.pl
new file mode 100755
index 0000000000..890e51e587
--- /dev/null
+++ b/scripts/copyrighter-sql.pl
@@ -0,0 +1,33 @@
+#!/usr/bin/perl -pi
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+$copyright = <<EOT;
+/* Copyright 2015 OpenMarket Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+EOT
+
+s/^(# -\*- coding: utf-8 -\*-\n)?/$1$copyright/ if ($. == 1);
diff --git a/database-prepare-for-0.0.1.sh b/scripts/database-prepare-for-0.0.1.sh
index 43d759a5cd..43d759a5cd 100755
--- a/database-prepare-for-0.0.1.sh
+++ b/scripts/database-prepare-for-0.0.1.sh
diff --git a/database-prepare-for-0.5.0.sh b/scripts/database-prepare-for-0.5.0.sh
index e824cb583e..e824cb583e 100755
--- a/database-prepare-for-0.5.0.sh
+++ b/scripts/database-prepare-for-0.5.0.sh
diff --git a/database-save.sh b/scripts/database-save.sh
index 040c8a4943..040c8a4943 100755
--- a/database-save.sh
+++ b/scripts/database-save.sh
diff --git a/nuke-room-from-db.sh b/scripts/nuke-room-from-db.sh
index 58c036c896..58c036c896 100755
--- a/nuke-room-from-db.sh
+++ b/scripts/nuke-room-from-db.sh
diff --git a/sphinx_api_docs.sh b/scripts/sphinx_api_docs.sh
index ee72b29657..ee72b29657 100644
--- a/sphinx_api_docs.sh
+++ b/scripts/sphinx_api_docs.sh
diff --git a/setup.cfg b/setup.cfg
index 2830831f00..888ad6ed4a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,3 +8,11 @@ test = trial
 
 [trial]
 test_suite = tests
+
+[check-manifest]
+ignore =
+    contrib
+    contrib/*
+    docs/*
+    pylint.cfg
+    tox.ini
diff --git a/synapse/__init__.py b/synapse/__init__.py
index e7e27b06ec..895a0766d2 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-""" This is a reference implementation of a synapse home server.
+""" This is a reference implementation of a Matrix home server.
 """
 
 __version__ = "0.6.1b"
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index e31482cfaa..a342a0e0da 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -21,6 +21,7 @@ from synapse.api.constants import EventTypes, Membership, JoinRules
 from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
 from synapse.util.logutils import log_function
 from synapse.util.async import run_on_reactor
+from synapse.types import UserID
 
 import logging
 
@@ -104,7 +105,7 @@ class Auth(object):
         for event in curr_state:
             if event.type == EventTypes.Member:
                 try:
-                    if self.hs.parse_userid(event.state_key).domain != host:
+                    if UserID.from_string(event.state_key).domain != host:
                         continue
                 except:
                     logger.warn("state_key not user_id: %s", event.state_key)
@@ -337,7 +338,7 @@ class Auth(object):
             user_info = {
                 "admin": bool(ret.get("admin", False)),
                 "device_id": ret.get("device_id"),
-                "user": self.hs.parse_userid(ret.get("name")),
+                "user": UserID.from_string(ret.get("name")),
             }
 
             defer.returnValue(user_info)
@@ -461,7 +462,7 @@ class Auth(object):
                             "You are not allowed to set others state"
                         )
                     else:
-                        sender_domain = self.hs.parse_userid(
+                        sender_domain = UserID.from_string(
                             event.user_id
                         ).domain
 
@@ -496,7 +497,7 @@ class Auth(object):
         # Validate users
         for k, v in user_list.items():
             try:
-                self.hs.parse_userid(k)
+                UserID.from_string(k)
             except:
                 raise SynapseError(400, "Not a valid user_id: %s" % (k,))
 
diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index 2b049debf3..5872e82d0f 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
 
 
 class Codes(object):
+    UNRECOGNIZED = "M_UNRECOGNIZED"
     UNAUTHORIZED = "M_UNAUTHORIZED"
     FORBIDDEN = "M_FORBIDDEN"
     BAD_JSON = "M_BAD_JSON"
@@ -34,6 +35,7 @@ class Codes(object):
     LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
     CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
     CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
+    MISSING_PARAM = "M_MISSING_PARAM",
     TOO_LARGE = "M_TOO_LARGE"
 
 
@@ -81,6 +83,34 @@ class RegistrationError(SynapseError):
     pass
 
 
+class UnrecognizedRequestError(SynapseError):
+    """An error indicating we don't understand the request you're trying to make"""
+    def __init__(self, *args, **kwargs):
+        if "errcode" not in kwargs:
+            kwargs["errcode"] = Codes.UNRECOGNIZED
+        message = None
+        if len(args) == 0:
+            message = "Unrecognized request"
+        else:
+            message = args[0]
+        super(UnrecognizedRequestError, self).__init__(
+            400,
+            message,
+            **kwargs
+        )
+
+
+class NotFoundError(SynapseError):
+    """An error indicating we can't find the thing you asked for"""
+    def __init__(self, *args, **kwargs):
+        if "errcode" not in kwargs:
+            kwargs["errcode"] = Codes.NOT_FOUND
+        super(NotFoundError, self).__init__(
+            404,
+            "Not found",
+            **kwargs
+        )
+
 class AuthError(SynapseError):
     """An error raised when there was a problem authorising an event."""
 
diff --git a/synapse/api/urls.py b/synapse/api/urls.py
index a299392049..693c0efda6 100644
--- a/synapse/api/urls.py
+++ b/synapse/api/urls.py
@@ -16,6 +16,7 @@
 """Contains the URL paths to prefix various aspects of the server with. """
 
 CLIENT_PREFIX = "/_matrix/client/api/v1"
+CLIENT_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha"
 FEDERATION_PREFIX = "/_matrix/federation/v1"
 WEB_CLIENT_PREFIX = "/_matrix/client"
 CONTENT_REPO_PREFIX = "/_matrix/content"
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 5fec8da7ca..f20ccfb5b6 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -18,27 +18,33 @@ from synapse.storage import prepare_database, UpgradeDatabaseException
 
 from synapse.server import HomeServer
 
+from synapse.python_dependencies import check_requirements
+
 from twisted.internet import reactor
 from twisted.enterprise import adbapi
 from twisted.web.resource import Resource
 from twisted.web.static import File
 from twisted.web.server import Site
 from synapse.http.server import JsonResource, RootRedirect
-from synapse.media.v0.content_repository import ContentRepoResource
-from synapse.media.v1.media_repository import MediaRepositoryResource
+from synapse.rest.media.v0.content_repository import ContentRepoResource
+from synapse.rest.media.v1.media_repository import MediaRepositoryResource
 from synapse.http.server_key_resource import LocalKey
 from synapse.http.matrixfederationclient import MatrixFederationHttpClient
 from synapse.api.urls import (
     CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
-    SERVER_KEY_PREFIX, MEDIA_PREFIX
+    SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX,
 )
 from synapse.config.homeserver import HomeServerConfig
 from synapse.crypto import context_factory
 from synapse.util.logcontext import LoggingContext
+from synapse.rest.client.v1 import ClientV1RestResource
+from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
 
 from daemonize import Daemonize
 import twisted.manhole.telnet
 
+import synapse
+
 import logging
 import os
 import re
@@ -55,7 +61,10 @@ class SynapseHomeServer(HomeServer):
         return MatrixFederationHttpClient(self)
 
     def build_resource_for_client(self):
-        return JsonResource()
+        return ClientV1RestResource(self)
+
+    def build_resource_for_client_v2_alpha(self):
+        return ClientV2AlphaRestResource(self)
 
     def build_resource_for_federation(self):
         return JsonResource()
@@ -100,6 +109,7 @@ class SynapseHomeServer(HomeServer):
         # [ ("/aaa/bbb/cc", Resource1), ("/aaa/dummy", Resource2) ]
         desired_tree = [
             (CLIENT_PREFIX, self.get_resource_for_client()),
+            (CLIENT_V2_ALPHA_PREFIX, self.get_resource_for_client_v2_alpha()),
             (FEDERATION_PREFIX, self.get_resource_for_federation()),
             (CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
             (SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
@@ -198,7 +208,10 @@ def setup():
 
     config.setup_logging()
 
+    check_requirements()
+
     logger.info("Server hostname: %s", config.server_name)
+    logger.info("Server version: %s", synapse.__version__)
 
     if re.search(":[0-9]+$", config.server_name):
         domain_with_port = config.server_name
@@ -217,8 +230,6 @@ def setup():
         content_addr=config.content_addr,
     )
 
-    hs.register_servlets()
-
     hs.create_resource_tree(
         web_client=config.webclient,
         redirect_root_to_web_client=True,
@@ -234,13 +245,20 @@ def setup():
     except UpgradeDatabaseException:
         sys.stderr.write(
             "\nFailed to upgrade database.\n"
-            "Have you checked for version specific instructions in UPGRADES.rst?\n"
+            "Have you checked for version specific instructions in"
+            " UPGRADES.rst?\n"
         )
         sys.exit(1)
 
     logger.info("Database prepared in %s.", db_name)
 
-    hs.get_db_pool()
+    db_pool = hs.get_db_pool()
+
+    if db_name == ":memory:":
+        # Memory databases will need to be setup each time they are opened.
+        reactor.callWhenRunning(
+            db_pool.runWithConnection, prepare_database
+        )
 
     if config.manhole:
         f = twisted.manhole.telnet.ShellFactory()
@@ -254,6 +272,8 @@ def setup():
         bind_port = None
     hs.start_listening(bind_port, config.unsecure_port)
 
+    hs.get_pusherpool().start()
+
     if config.daemonize:
         print config.pid_file
         daemon = Daemonize(
@@ -277,6 +297,7 @@ def run():
 
 def main():
     with LoggingContext("main"):
+        check_requirements()
         setup()
 
 
diff --git a/synapse/config/database.py b/synapse/config/database.py
index 0d33583a7d..daa161c952 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -20,7 +20,10 @@ import os
 class DatabaseConfig(Config):
     def __init__(self, args):
         super(DatabaseConfig, self).__init__(args)
-        self.database_path = self.abspath(args.database_path)
+        if args.database_path == ":memory:":
+            self.database_path = ":memory:"
+        else:
+            self.database_path = self.abspath(args.database_path)
 
     @classmethod
     def add_arguments(cls, parser):
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index 15383b3184..f9568ebd21 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -66,7 +66,10 @@ class LoggingConfig(Config):
 
             formatter = logging.Formatter(log_format)
             if self.log_file:
-                handler = logging.FileHandler(self.log_file)
+                # TODO: Customisable file size / backup count
+                handler = logging.handlers.RotatingFileHandler(
+                    self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
+                )
             else:
                 handler = logging.StreamHandler()
             handler.setFormatter(formatter)
diff --git a/synapse/config/server.py b/synapse/config/server.py
index 4f73c85466..31e44cc857 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -47,8 +47,12 @@ class ServerConfig(Config):
     def add_arguments(cls, parser):
         super(ServerConfig, cls).add_arguments(parser)
         server_group = parser.add_argument_group("server")
-        server_group.add_argument("-H", "--server-name", default="localhost",
-                                  help="The name of the server")
+        server_group.add_argument(
+            "-H", "--server-name", default="localhost",
+            help="The domain name of the server, with optional explicit port. "
+                 "This is used by remote servers to connect to this server, "
+                 "e.g. matrix.org, localhost:8080, etc."
+        )
         server_group.add_argument("--signing-key-path",
                                   help="The signing key to sign messages with")
         server_group.add_argument("-p", "--bind-port", metavar="PORT",
diff --git a/synapse/events/builder.py b/synapse/events/builder.py
index d4cb602ebb..a9b1b99a10 100644
--- a/synapse/events/builder.py
+++ b/synapse/events/builder.py
@@ -33,12 +33,6 @@ class EventBuilder(EventBase):
             unsigned=unsigned
         )
 
-    def update_event_key(self, key, value):
-        self._event_dict[key] = value
-
-    def update_event_keys(self, other_dict):
-        self._event_dict.update(other_dict)
-
     def build(self):
         return FrozenEvent.from_event(self)
 
diff --git a/synapse/events/utils.py b/synapse/events/utils.py
index 4ad37188ba..e391aca4cc 100644
--- a/synapse/events/utils.py
+++ b/synapse/events/utils.py
@@ -89,23 +89,31 @@ def prune_event(event):
     return type(event)(allowed_fields)
 
 
-def serialize_event(hs, e):
+def serialize_event(e, time_now_ms, client_event=True):
     # FIXME(erikj): To handle the case of presence events and the like
     if not isinstance(e, EventBase):
         return e
 
+    time_now_ms = int(time_now_ms)
+
     # Should this strip out None's?
     d = {k: v for k, v in e.get_dict().items()}
+
+    if not client_event:
+        # set the age and keep all other keys
+        if "age_ts" in d["unsigned"]:
+            d["unsigned"]["age"] = time_now_ms - d["unsigned"]["age_ts"]
+        return d
+
     if "age_ts" in d["unsigned"]:
-        now = int(hs.get_clock().time_msec())
-        d["age"] = now - d["unsigned"]["age_ts"]
+        d["age"] = time_now_ms - d["unsigned"]["age_ts"]
         del d["unsigned"]["age_ts"]
 
     d["user_id"] = d.pop("sender", None)
 
     if "redacted_because" in e.unsigned:
         d["redacted_because"] = serialize_event(
-            hs, e.unsigned["redacted_because"]
+            e.unsigned["redacted_because"], time_now_ms
         )
 
         del d["unsigned"]["redacted_because"]
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index a4c29b484b..6620532a60 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -256,23 +256,21 @@ class ReplicationLayer(object):
 
     @defer.inlineCallbacks
     @log_function
-    def get_state_for_context(self, destination, context, event_id):
-        """Requests all of the `current` state PDUs for a given context from
+    def get_state_for_room(self, destination, room_id, event_id):
+        """Requests all of the `current` state PDUs for a given room from
         a remote home server.
 
         Args:
             destination (str): The remote homeserver to query for the state.
-            context (str): The context we're interested in.
+            room_id (str): The id of the room we're interested in.
             event_id (str): The id of the event we want the state at.
 
         Returns:
             Deferred: Results in a list of PDUs.
         """
 
-        result = yield self.transport_layer.get_context_state(
-            destination,
-            context,
-            event_id=event_id,
+        result = yield self.transport_layer.get_room_state(
+            destination, room_id, event_id=event_id,
         )
 
         pdus = [
@@ -288,9 +286,9 @@ class ReplicationLayer(object):
 
     @defer.inlineCallbacks
     @log_function
-    def get_event_auth(self, destination, context, event_id):
+    def get_event_auth(self, destination, room_id, event_id):
         res = yield self.transport_layer.get_event_auth(
-            destination, context, event_id,
+            destination, room_id, event_id,
         )
 
         auth_chain = [
@@ -304,9 +302,9 @@ class ReplicationLayer(object):
 
     @defer.inlineCallbacks
     @log_function
-    def on_backfill_request(self, origin, context, versions, limit):
+    def on_backfill_request(self, origin, room_id, versions, limit):
         pdus = yield self.handler.on_backfill_request(
-            origin, context, versions, limit
+            origin, room_id, versions, limit
         )
 
         defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
@@ -380,12 +378,10 @@ class ReplicationLayer(object):
 
     @defer.inlineCallbacks
     @log_function
-    def on_context_state_request(self, origin, context, event_id):
+    def on_context_state_request(self, origin, room_id, event_id):
         if event_id:
             pdus = yield self.handler.get_state_for_pdu(
-                origin,
-                context,
-                event_id,
+                origin, room_id, event_id,
             )
             auth_chain = yield self.store.get_auth_chain(
                 [pdu.event_id for pdu in pdus]
@@ -413,7 +409,7 @@ class ReplicationLayer(object):
     @defer.inlineCallbacks
     @log_function
     def on_pull_request(self, origin, versions):
-        raise NotImplementedError("Pull transacions not implemented")
+        raise NotImplementedError("Pull transactions not implemented")
 
     @defer.inlineCallbacks
     def on_query_request(self, query_type, args):
@@ -422,30 +418,21 @@ class ReplicationLayer(object):
             defer.returnValue((200, response))
         else:
             defer.returnValue(
-                (404, "No handler for Query type '%s'" % (query_type, ))
+                (404, "No handler for Query type '%s'" % (query_type,))
             )
 
     @defer.inlineCallbacks
-    def on_make_join_request(self, context, user_id):
-        pdu = yield self.handler.on_make_join_request(context, user_id)
+    def on_make_join_request(self, room_id, user_id):
+        pdu = yield self.handler.on_make_join_request(room_id, user_id)
         time_now = self._clock.time_msec()
-        defer.returnValue({
-            "event": pdu.get_pdu_json(time_now),
-        })
+        defer.returnValue({"event": pdu.get_pdu_json(time_now)})
 
     @defer.inlineCallbacks
     def on_invite_request(self, origin, content):
         pdu = self.event_from_pdu_json(content)
         ret_pdu = yield self.handler.on_invite_request(origin, pdu)
         time_now = self._clock.time_msec()
-        defer.returnValue(
-            (
-                200,
-                {
-                    "event": ret_pdu.get_pdu_json(time_now),
-                }
-            )
-        )
+        defer.returnValue((200, {"event": ret_pdu.get_pdu_json(time_now)}))
 
     @defer.inlineCallbacks
     def on_send_join_request(self, origin, content):
@@ -462,26 +449,17 @@ class ReplicationLayer(object):
         }))
 
     @defer.inlineCallbacks
-    def on_event_auth(self, origin, context, event_id):
+    def on_event_auth(self, origin, room_id, event_id):
         time_now = self._clock.time_msec()
         auth_pdus = yield self.handler.on_event_auth(event_id)
-        defer.returnValue(
-            (
-                200,
-                {
-                    "auth_chain": [
-                        a.get_pdu_json(time_now) for a in auth_pdus
-                    ],
-                }
-            )
-        )
+        defer.returnValue((200, {
+            "auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus],
+        }))
 
     @defer.inlineCallbacks
-    def make_join(self, destination, context, user_id):
+    def make_join(self, destination, room_id, user_id):
         ret = yield self.transport_layer.make_join(
-            destination=destination,
-            context=context,
-            user_id=user_id,
+            destination, room_id, user_id
         )
 
         pdu_dict = ret["event"]
@@ -494,10 +472,10 @@ class ReplicationLayer(object):
     def send_join(self, destination, pdu):
         time_now = self._clock.time_msec()
         _, content = yield self.transport_layer.send_join(
-            destination,
-            pdu.room_id,
-            pdu.event_id,
-            pdu.get_pdu_json(time_now),
+            destination=destination,
+            room_id=pdu.room_id,
+            event_id=pdu.event_id,
+            content=pdu.get_pdu_json(time_now),
         )
 
         logger.debug("Got content: %s", content)
@@ -507,9 +485,6 @@ class ReplicationLayer(object):
             for p in content.get("state", [])
         ]
 
-        # FIXME: We probably want to do something with the auth_chain given
-        # to us
-
         auth_chain = [
             self.event_from_pdu_json(p, outlier=True)
             for p in content.get("auth_chain", [])
@@ -523,11 +498,11 @@ class ReplicationLayer(object):
         })
 
     @defer.inlineCallbacks
-    def send_invite(self, destination, context, event_id, pdu):
+    def send_invite(self, destination, room_id, event_id, pdu):
         time_now = self._clock.time_msec()
         code, content = yield self.transport_layer.send_invite(
             destination=destination,
-            context=context,
+            room_id=room_id,
             event_id=event_id,
             content=pdu.get_pdu_json(time_now),
         )
@@ -657,7 +632,7 @@ class ReplicationLayer(object):
                     "_handle_new_pdu getting state for %s",
                     pdu.room_id
                 )
-                state, auth_chain = yield self.get_state_for_context(
+                state, auth_chain = yield self.get_state_for_room(
                     origin, pdu.room_id, pdu.event_id,
                 )
 
@@ -816,7 +791,7 @@ class _TransactionQueue(object):
                 logger.info("TX [%s] is ready for retry", destination)
 
         logger.info("TX [%s] _attempt_new_transaction", destination)
-        
+
         if destination in self.pending_transactions:
             # XXX: pending_transactions can get stuck on by a never-ending
             # request at which point pending_pdus_by_dest just keeps growing.
@@ -830,14 +805,15 @@ class _TransactionQueue(object):
         pending_failures = self.pending_failures_by_dest.pop(destination, [])
 
         if pending_pdus:
-            logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d", destination, len(pending_pdus))
+            logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
+                        destination, len(pending_pdus))
 
         if not pending_pdus and not pending_edus and not pending_failures:
             return
 
         logger.debug(
-            "TX [%s] Attempting new transaction "
-            "(pdus: %d, edus: %d, failures: %d)",
+            "TX [%s] Attempting new transaction"
+            " (pdus: %d, edus: %d, failures: %d)",
             destination,
             len(pending_pdus),
             len(pending_edus),
diff --git a/synapse/federation/transport/__init__.py b/synapse/federation/transport/__init__.py
new file mode 100644
index 0000000000..6800ac46c5
--- /dev/null
+++ b/synapse/federation/transport/__init__.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""The transport layer is responsible for both sending transactions to remote
+home servers and receiving a variety of requests from other home servers.
+
+By default this is done over HTTPS (and all home servers are required to
+support HTTPS), however individual pairings of servers may decide to
+communicate over a different (albeit still reliable) protocol.
+"""
+
+from .server import TransportLayerServer
+from .client import TransportLayerClient
+
+
+class TransportLayer(TransportLayerServer, TransportLayerClient):
+    """This is a basic implementation of the transport layer that translates
+    transactions and other requests to/from HTTP.
+
+    Attributes:
+        server_name (str): Local home server host
+
+        server (synapse.http.server.HttpServer): the http server to
+                register listeners on
+
+        client (synapse.http.client.HttpClient): the http client used to
+                send requests
+
+        request_handler (TransportRequestHandler): The handler to fire when we
+            receive requests for data.
+
+        received_handler (TransportReceivedHandler): The handler to fire when
+            we receive data.
+    """
+
+    def __init__(self, homeserver, server_name, server, client):
+        """
+        Args:
+            server_name (str): Local home server host
+            server (synapse.protocol.http.HttpServer): the http server to
+                register listeners on
+            client (synapse.protocol.http.HttpClient): the http client used to
+                send requests
+        """
+        self.keyring = homeserver.get_keyring()
+        self.server_name = server_name
+        self.server = server
+        self.client = client
+        self.request_handler = None
+        self.received_handler = None
diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py
new file mode 100644
index 0000000000..e634a3a213
--- /dev/null
+++ b/synapse/federation/transport/client.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from synapse.api.urls import FEDERATION_PREFIX as PREFIX
+from synapse.util.logutils import log_function
+
+import logging
+import json
+
+
+logger = logging.getLogger(__name__)
+
+
+class TransportLayerClient(object):
+    """Sends federation HTTP requests to other servers"""
+
+    @log_function
+    def get_room_state(self, destination, room_id, event_id):
+        """ Requests all state for a given room from the given server at the
+        given event.
+
+        Args:
+            destination (str): The host name of the remote home server we want
+                to get the state from.
+            context (str): The name of the context we want the state of
+            event_id (str): The event we want the context at.
+
+        Returns:
+            Deferred: Results in a dict received from the remote homeserver.
+        """
+        logger.debug("get_room_state dest=%s, room=%s",
+                     destination, room_id)
+
+        path = PREFIX + "/state/%s/" % room_id
+        return self.client.get_json(
+            destination, path=path, args={"event_id": event_id},
+        )
+
+    @log_function
+    def get_event(self, destination, event_id):
+        """ Requests the pdu with give id and origin from the given server.
+
+        Args:
+            destination (str): The host name of the remote home server we want
+                to get the state from.
+            event_id (str): The id of the event being requested.
+
+        Returns:
+            Deferred: Results in a dict received from the remote homeserver.
+        """
+        logger.debug("get_pdu dest=%s, event_id=%s",
+                     destination, event_id)
+
+        path = PREFIX + "/event/%s/" % (event_id, )
+        return self.client.get_json(destination, path=path)
+
+    @log_function
+    def backfill(self, destination, room_id, event_tuples, limit):
+        """ Requests `limit` previous PDUs in a given context before list of
+        PDUs.
+
+        Args:
+            dest (str)
+            room_id (str)
+            event_tuples (list)
+            limt (int)
+
+        Returns:
+            Deferred: Results in a dict received from the remote homeserver.
+        """
+        logger.debug(
+            "backfill dest=%s, room_id=%s, event_tuples=%s, limit=%s",
+            destination, room_id, repr(event_tuples), str(limit)
+        )
+
+        if not event_tuples:
+            # TODO: raise?
+            return
+
+        path = PREFIX + "/backfill/%s/" % (room_id,)
+
+        args = {
+            "v": event_tuples,
+            "limit": [str(limit)],
+        }
+
+        return self.client.get_json(
+            destination,
+            path=path,
+            args=args,
+        )
+
+    @defer.inlineCallbacks
+    @log_function
+    def send_transaction(self, transaction, json_data_callback=None):
+        """ Sends the given Transaction to its destination
+
+        Args:
+            transaction (Transaction)
+
+        Returns:
+            Deferred: Results of the deferred is a tuple in the form of
+            (response_code, response_body) where the response_body is a
+            python dict decoded from json
+        """
+        logger.debug(
+            "send_data dest=%s, txid=%s",
+            transaction.destination, transaction.transaction_id
+        )
+
+        if transaction.destination == self.server_name:
+            raise RuntimeError("Transport layer cannot send to itself!")
+
+        # FIXME: This is only used by the tests. The actual json sent is
+        # generated by the json_data_callback.
+        json_data = transaction.get_dict()
+
+        code, response = yield self.client.put_json(
+            transaction.destination,
+            path=PREFIX + "/send/%s/" % transaction.transaction_id,
+            data=json_data,
+            json_data_callback=json_data_callback,
+        )
+
+        logger.debug(
+            "send_data dest=%s, txid=%s, got response: %d",
+            transaction.destination, transaction.transaction_id, code
+        )
+
+        defer.returnValue((code, response))
+
+    @defer.inlineCallbacks
+    @log_function
+    def make_query(self, destination, query_type, args, retry_on_dns_fail):
+        path = PREFIX + "/query/%s" % query_type
+
+        response = yield self.client.get_json(
+            destination=destination,
+            path=path,
+            args=args,
+            retry_on_dns_fail=retry_on_dns_fail,
+        )
+
+        defer.returnValue(response)
+
+    @defer.inlineCallbacks
+    @log_function
+    def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True):
+        path = PREFIX + "/make_join/%s/%s" % (room_id, user_id)
+
+        response = yield self.client.get_json(
+            destination=destination,
+            path=path,
+            retry_on_dns_fail=retry_on_dns_fail,
+        )
+
+        defer.returnValue(response)
+
+    @defer.inlineCallbacks
+    @log_function
+    def send_join(self, destination, room_id, event_id, content):
+        path = PREFIX + "/send_join/%s/%s" % (room_id, event_id)
+
+        code, content = yield self.client.put_json(
+            destination=destination,
+            path=path,
+            data=content,
+        )
+
+        if not 200 <= code < 300:
+            raise RuntimeError("Got %d from send_join", code)
+
+        defer.returnValue(json.loads(content))
+
+    @defer.inlineCallbacks
+    @log_function
+    def send_invite(self, destination, room_id, event_id, content):
+        path = PREFIX + "/invite/%s/%s" % (room_id, event_id)
+
+        code, content = yield self.client.put_json(
+            destination=destination,
+            path=path,
+            data=content,
+        )
+
+        if not 200 <= code < 300:
+            raise RuntimeError("Got %d from send_invite", code)
+
+        defer.returnValue(json.loads(content))
+
+    @defer.inlineCallbacks
+    @log_function
+    def get_event_auth(self, destination, room_id, event_id):
+        path = PREFIX + "/event_auth/%s/%s" % (room_id, event_id)
+
+        response = yield self.client.get_json(
+            destination=destination,
+            path=path,
+        )
+
+        defer.returnValue(response)
diff --git a/synapse/federation/transport.py b/synapse/federation/transport/server.py
index 1f0f06e0fe..a380a6910b 100644
--- a/synapse/federation/transport.py
+++ b/synapse/federation/transport/server.py
@@ -13,14 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""The transport layer is responsible for both sending transactions to remote
-home servers and receiving a variety of requests from other home servers.
-
-Typically, this is done over HTTP (and all home servers are required to
-support HTTP), however individual pairings of servers may decide to communicate
-over a different (albeit still reliable) protocol.
-"""
-
 from twisted.internet import defer
 
 from synapse.api.urls import FEDERATION_PREFIX as PREFIX
@@ -35,241 +27,8 @@ import re
 logger = logging.getLogger(__name__)
 
 
-class TransportLayer(object):
-    """This is a basic implementation of the transport layer that translates
-    transactions and other requests to/from HTTP.
-
-    Attributes:
-        server_name (str): Local home server host
-
-        server (synapse.http.server.HttpServer): the http server to
-                register listeners on
-
-        client (synapse.http.client.HttpClient): the http client used to
-                send requests
-
-        request_handler (TransportRequestHandler): The handler to fire when we
-            receive requests for data.
-
-        received_handler (TransportReceivedHandler): The handler to fire when
-            we receive data.
-    """
-
-    def __init__(self, homeserver, server_name, server, client):
-        """
-        Args:
-            server_name (str): Local home server host
-            server (synapse.protocol.http.HttpServer): the http server to
-                register listeners on
-            client (synapse.protocol.http.HttpClient): the http client used to
-                send requests
-        """
-        self.keyring = homeserver.get_keyring()
-        self.server_name = server_name
-        self.server = server
-        self.client = client
-        self.request_handler = None
-        self.received_handler = None
-
-    @log_function
-    def get_context_state(self, destination, context, event_id=None):
-        """ Requests all state for a given context (i.e. room) from the
-        given server.
-
-        Args:
-            destination (str): The host name of the remote home server we want
-                to get the state from.
-            context (str): The name of the context we want the state of
-
-        Returns:
-            Deferred: Results in a dict received from the remote homeserver.
-        """
-        logger.debug("get_context_state dest=%s, context=%s",
-                     destination, context)
-
-        subpath = "/state/%s/" % context
-
-        args = {}
-        if event_id:
-            args["event_id"] = event_id
-
-        return self._do_request_for_transaction(
-            destination, subpath, args=args
-        )
-
-    @log_function
-    def get_event(self, destination, event_id):
-        """ Requests the pdu with give id and origin from the given server.
-
-        Args:
-            destination (str): The host name of the remote home server we want
-                to get the state from.
-            event_id (str): The id of the event being requested.
-
-        Returns:
-            Deferred: Results in a dict received from the remote homeserver.
-        """
-        logger.debug("get_pdu dest=%s, event_id=%s",
-                     destination, event_id)
-
-        subpath = "/event/%s/" % (event_id, )
-
-        return self._do_request_for_transaction(destination, subpath)
-
-    @log_function
-    def backfill(self, dest, context, event_tuples, limit):
-        """ Requests `limit` previous PDUs in a given context before list of
-        PDUs.
-
-        Args:
-            dest (str)
-            context (str)
-            event_tuples (list)
-            limt (int)
-
-        Returns:
-            Deferred: Results in a dict received from the remote homeserver.
-        """
-        logger.debug(
-            "backfill dest=%s, context=%s, event_tuples=%s, limit=%s",
-            dest, context, repr(event_tuples), str(limit)
-        )
-
-        if not event_tuples:
-            # TODO: raise?
-            return
-
-        subpath = "/backfill/%s/" % (context,)
-
-        args = {
-            "v": event_tuples,
-            "limit": [str(limit)],
-        }
-
-        return self._do_request_for_transaction(
-            dest,
-            subpath,
-            args=args,
-        )
-
-    @defer.inlineCallbacks
-    @log_function
-    def send_transaction(self, transaction, json_data_callback=None):
-        """ Sends the given Transaction to its destination
-
-        Args:
-            transaction (Transaction)
-
-        Returns:
-            Deferred: Results of the deferred is a tuple in the form of
-            (response_code, response_body) where the response_body is a
-            python dict decoded from json
-        """
-        logger.debug(
-            "send_data dest=%s, txid=%s",
-            transaction.destination, transaction.transaction_id
-        )
-
-        if transaction.destination == self.server_name:
-            raise RuntimeError("Transport layer cannot send to itself!")
-
-        # FIXME: This is only used by the tests. The actual json sent is
-        # generated by the json_data_callback.
-        json_data = transaction.get_dict()
-
-        code, response = yield self.client.put_json(
-            transaction.destination,
-            path=PREFIX + "/send/%s/" % transaction.transaction_id,
-            data=json_data,
-            json_data_callback=json_data_callback,
-        )
-
-        logger.debug(
-            "send_data dest=%s, txid=%s, got response: %d",
-            transaction.destination, transaction.transaction_id, code
-        )
-
-        defer.returnValue((code, response))
-
-    @defer.inlineCallbacks
-    @log_function
-    def make_query(self, destination, query_type, args, retry_on_dns_fail):
-        path = PREFIX + "/query/%s" % query_type
-
-        response = yield self.client.get_json(
-            destination=destination,
-            path=path,
-            args=args,
-            retry_on_dns_fail=retry_on_dns_fail,
-        )
-
-        defer.returnValue(response)
-
-    @defer.inlineCallbacks
-    @log_function
-    def make_join(self, destination, context, user_id, retry_on_dns_fail=True):
-        path = PREFIX + "/make_join/%s/%s" % (context, user_id,)
-
-        response = yield self.client.get_json(
-            destination=destination,
-            path=path,
-            retry_on_dns_fail=retry_on_dns_fail,
-        )
-
-        defer.returnValue(response)
-
-    @defer.inlineCallbacks
-    @log_function
-    def send_join(self, destination, context, event_id, content):
-        path = PREFIX + "/send_join/%s/%s" % (
-            context,
-            event_id,
-        )
-
-        code, content = yield self.client.put_json(
-            destination=destination,
-            path=path,
-            data=content,
-        )
-
-        if not 200 <= code < 300:
-            raise RuntimeError("Got %d from send_join", code)
-
-        defer.returnValue(json.loads(content))
-
-    @defer.inlineCallbacks
-    @log_function
-    def send_invite(self, destination, context, event_id, content):
-        path = PREFIX + "/invite/%s/%s" % (
-            context,
-            event_id,
-        )
-
-        code, content = yield self.client.put_json(
-            destination=destination,
-            path=path,
-            data=content,
-        )
-
-        if not 200 <= code < 300:
-            raise RuntimeError("Got %d from send_invite", code)
-
-        defer.returnValue(json.loads(content))
-
-    @defer.inlineCallbacks
-    @log_function
-    def get_event_auth(self, destination, context, event_id):
-        path = PREFIX + "/event_auth/%s/%s" % (
-            context,
-            event_id,
-        )
-
-        response = yield self.client.get_json(
-            destination=destination,
-            path=path,
-        )
-
-        defer.returnValue(response)
+class TransportLayerServer(object):
+    """Handles incoming federation HTTP requests"""
 
     @defer.inlineCallbacks
     def _authenticate_request(self, request):
@@ -373,8 +132,6 @@ class TransportLayer(object):
         """
         self.request_handler = handler
 
-        # TODO(markjh): Namespace the federation URI paths
-
         # This is for when someone asks us for everything since version X
         self.server.register_path(
             "GET",
@@ -528,34 +285,6 @@ class TransportLayer(object):
 
         defer.returnValue((code, response))
 
-    @defer.inlineCallbacks
-    @log_function
-    def _do_request_for_transaction(self, destination, subpath, args={}):
-        """
-        Args:
-            destination (str)
-            path (str)
-            args (dict): This is parsed directly to the HttpClient.
-
-        Returns:
-            Deferred: Results in a dict.
-        """
-
-        data = yield self.client.get_json(
-            destination,
-            path=PREFIX + subpath,
-            args=args,
-        )
-
-        # Add certain keys to the JSON, ready for decoding as a Transaction
-        data.update(
-            origin=destination,
-            destination=self.server_name,
-            transaction_id=None
-        )
-
-        defer.returnValue(data)
-
     @log_function
     def _on_backfill_request(self, origin, context, v_list, limits):
         if not limits:
diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index 38af034b4d..1773fa20aa 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -19,6 +19,7 @@ from synapse.api.errors import LimitExceededError, SynapseError
 from synapse.util.async import run_on_reactor
 from synapse.crypto.event_signing import add_hashes_and_signatures
 from synapse.api.constants import Membership, EventTypes
+from synapse.types import UserID
 
 import logging
 
@@ -113,7 +114,7 @@ class BaseHandler(object):
 
         if event.type == EventTypes.Member:
             if event.content["membership"] == Membership.INVITE:
-                invitee = self.hs.parse_userid(event.state_key)
+                invitee = UserID.from_string(event.state_key)
                 if not self.hs.is_mine(invitee):
                     # TODO: Can we add signature from remote server in a nicer
                     # way? If we have been invited by a remote server, we need
@@ -134,7 +135,7 @@ class BaseHandler(object):
                 if k[0] == EventTypes.Member:
                     if s.content["membership"] == Membership.JOIN:
                         destinations.add(
-                            self.hs.parse_userid(s.state_key).domain
+                            UserID.from_string(s.state_key).domain
                         )
             except SynapseError:
                 logger.warn(
@@ -144,7 +145,5 @@ class BaseHandler(object):
         yield self.notifier.on_new_room_event(event, extra_users=extra_users)
 
         yield federation_handler.handle_new_event(
-            event,
-            None,
-            destinations=destinations,
+            event, destinations=destinations,
         )
diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py
index 91fceda2ac..58e9a91562 100644
--- a/synapse/handlers/directory.py
+++ b/synapse/handlers/directory.py
@@ -19,6 +19,7 @@ from ._base import BaseHandler
 
 from synapse.api.errors import SynapseError, Codes, CodeMessageException
 from synapse.api.constants import EventTypes
+from synapse.types import RoomAlias
 
 import logging
 
@@ -122,7 +123,7 @@ class DirectoryHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def on_directory_query(self, args):
-        room_alias = self.hs.parse_roomalias(args["room_alias"])
+        room_alias = RoomAlias.from_string(args["room_alias"])
         if not self.hs.is_mine(room_alias):
             raise SynapseError(
                 400, "Room Alias is not hosted on this Home Server"
diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py
index 808219bd10..025e7e7e62 100644
--- a/synapse/handlers/events.py
+++ b/synapse/handlers/events.py
@@ -17,6 +17,8 @@ from twisted.internet import defer
 
 from synapse.util.logcontext import PreserveLoggingContext
 from synapse.util.logutils import log_function
+from synapse.types import UserID
+from synapse.events.utils import serialize_event
 
 from ._base import BaseHandler
 
@@ -46,39 +48,43 @@ class EventStreamHandler(BaseHandler):
 
     @defer.inlineCallbacks
     @log_function
-    def get_stream(self, auth_user_id, pagin_config, timeout=0):
-        auth_user = self.hs.parse_userid(auth_user_id)
+    def get_stream(self, auth_user_id, pagin_config, timeout=0,
+                   as_client_event=True, affect_presence=True):
+        auth_user = UserID.from_string(auth_user_id)
 
         try:
-            if auth_user not in self._streams_per_user:
-                self._streams_per_user[auth_user] = 0
-                if auth_user in self._stop_timer_per_user:
-                    try:
-                        self.clock.cancel_call_later(
-                            self._stop_timer_per_user.pop(auth_user)
+            if affect_presence:
+                if auth_user not in self._streams_per_user:
+                    self._streams_per_user[auth_user] = 0
+                    if auth_user in self._stop_timer_per_user:
+                        try:
+                            self.clock.cancel_call_later(
+                                self._stop_timer_per_user.pop(auth_user)
+                            )
+                        except:
+                            logger.exception("Failed to cancel event timer")
+                    else:
+                        yield self.distributor.fire(
+                            "started_user_eventstream", auth_user
                         )
-                    except:
-                        logger.exception("Failed to cancel event timer")
-                else:
-                    yield self.distributor.fire(
-                        "started_user_eventstream", auth_user
-                    )
-            self._streams_per_user[auth_user] += 1
+                self._streams_per_user[auth_user] += 1
 
             if pagin_config.from_token is None:
                 pagin_config.from_token = None
 
             rm_handler = self.hs.get_handlers().room_member_handler
-            logger.debug("BETA")
             room_ids = yield rm_handler.get_rooms_for_user(auth_user)
 
-            logger.debug("ALPHA")
             with PreserveLoggingContext():
                 events, tokens = yield self.notifier.get_events_for(
                     auth_user, room_ids, pagin_config, timeout
                 )
 
-            chunks = [self.hs.serialize_event(e) for e in events]
+            time_now = self.clock.time_msec()
+
+            chunks = [
+                serialize_event(e, time_now, as_client_event) for e in events
+            ]
 
             chunk = {
                 "chunk": chunks,
@@ -89,27 +95,28 @@ class EventStreamHandler(BaseHandler):
             defer.returnValue(chunk)
 
         finally:
-            self._streams_per_user[auth_user] -= 1
-            if not self._streams_per_user[auth_user]:
-                del self._streams_per_user[auth_user]
-
-                # 10 seconds of grace to allow the client to reconnect again
-                #   before we think they're gone
-                def _later():
-                    logger.debug(
-                        "_later stopped_user_eventstream %s", auth_user
-                    )
+            if affect_presence:
+                self._streams_per_user[auth_user] -= 1
+                if not self._streams_per_user[auth_user]:
+                    del self._streams_per_user[auth_user]
+
+                    # 10 seconds of grace to allow the client to reconnect again
+                    #   before we think they're gone
+                    def _later():
+                        logger.debug(
+                            "_later stopped_user_eventstream %s", auth_user
+                        )
 
-                    self._stop_timer_per_user.pop(auth_user, None)
+                        self._stop_timer_per_user.pop(auth_user, None)
 
-                    yield self.distributor.fire(
-                        "stopped_user_eventstream", auth_user
-                    )
+                        return self.distributor.fire(
+                            "stopped_user_eventstream", auth_user
+                        )
 
-                logger.debug("Scheduling _later: for %s", auth_user)
-                self._stop_timer_per_user[auth_user] = (
-                    self.clock.call_later(30, _later)
-                )
+                    logger.debug("Scheduling _later: for %s", auth_user)
+                    self._stop_timer_per_user[auth_user] = (
+                        self.clock.call_later(30, _later)
+                    )
 
 
 class EventHandler(BaseHandler):
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index d26975a88a..bcdcc90a18 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -28,6 +28,7 @@ from synapse.crypto.event_signing import (
     compute_event_signature, check_event_content_hash,
     add_hashes_and_signatures,
 )
+from synapse.types import UserID
 from syutil.jsonutil import encode_canonical_json
 
 from twisted.internet import defer
@@ -75,14 +76,14 @@ class FederationHandler(BaseHandler):
 
     @log_function
     @defer.inlineCallbacks
-    def handle_new_event(self, event, snapshot, destinations):
+    def handle_new_event(self, event, destinations):
         """ Takes in an event from the client to server side, that has already
         been authed and handled by the state module, and sends it to any
         remote home servers that may be interested.
 
         Args:
-            event
-            snapshot (.storage.Snapshot): THe snapshot the event happened after
+            event: The event to send
+            destinations: A list of destinations to send it to
 
         Returns:
             Deferred: Resolved when it has successfully been queued for
@@ -154,7 +155,7 @@ class FederationHandler(BaseHandler):
             replication = self.replication_layer
 
             if not state:
-                state, auth_chain = yield replication.get_state_for_context(
+                state, auth_chain = yield replication.get_state_for_room(
                     origin, context=event.room_id, event_id=event.event_id,
                 )
 
@@ -227,7 +228,7 @@ class FederationHandler(BaseHandler):
             extra_users = []
             if event.type == EventTypes.Member:
                 target_user_id = event.state_key
-                target_user = self.hs.parse_userid(target_user_id)
+                target_user = UserID.from_string(target_user_id)
                 extra_users.append(target_user)
 
             yield self.notifier.on_new_room_event(
@@ -236,7 +237,7 @@ class FederationHandler(BaseHandler):
 
         if event.type == EventTypes.Member:
             if event.membership == Membership.JOIN:
-                user = self.hs.parse_userid(event.state_key)
+                user = UserID.from_string(event.state_key)
                 yield self.distributor.fire(
                     "user_joined_room", user=user, room_id=event.room_id
                 )
@@ -281,7 +282,7 @@ class FederationHandler(BaseHandler):
         """
         pdu = yield self.replication_layer.send_invite(
             destination=target_host,
-            context=event.room_id,
+            room_id=event.room_id,
             event_id=event.event_id,
             pdu=event
         )
@@ -491,7 +492,7 @@ class FederationHandler(BaseHandler):
         extra_users = []
         if event.type == EventTypes.Member:
             target_user_id = event.state_key
-            target_user = self.hs.parse_userid(target_user_id)
+            target_user = UserID.from_string(target_user_id)
             extra_users.append(target_user)
 
         yield self.notifier.on_new_room_event(
@@ -500,7 +501,7 @@ class FederationHandler(BaseHandler):
 
         if event.type == EventTypes.Member:
             if event.content["membership"] == Membership.JOIN:
-                user = self.hs.parse_userid(event.state_key)
+                user = UserID.from_string(event.state_key)
                 yield self.distributor.fire(
                     "user_joined_room", user=user, room_id=event.room_id
                 )
@@ -514,7 +515,7 @@ class FederationHandler(BaseHandler):
                 if k[0] == EventTypes.Member:
                     if s.content["membership"] == Membership.JOIN:
                         destinations.add(
-                            self.hs.parse_userid(s.state_key).domain
+                            UserID.from_string(s.state_key).domain
                         )
             except:
                 logger.warn(
@@ -565,7 +566,7 @@ class FederationHandler(BaseHandler):
             backfilled=False,
         )
 
-        target_user = self.hs.parse_userid(event.state_key)
+        target_user = UserID.from_string(event.state_key)
         yield self.notifier.on_new_room_event(
             event, extra_users=[target_user],
         )
@@ -617,13 +618,13 @@ class FederationHandler(BaseHandler):
 
     @defer.inlineCallbacks
     @log_function
-    def on_backfill_request(self, origin, context, pdu_list, limit):
-        in_room = yield self.auth.check_host_in_room(context, origin)
+    def on_backfill_request(self, origin, room_id, pdu_list, limit):
+        in_room = yield self.auth.check_host_in_room(room_id, origin)
         if not in_room:
             raise AuthError(403, "Host not in room.")
 
         events = yield self.store.get_backfill_events(
-            context,
+            room_id,
             pdu_list,
             limit
         )
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 7195de98b5..9c3271fe88 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -18,8 +18,10 @@ from twisted.internet import defer
 from synapse.api.constants import EventTypes, Membership
 from synapse.api.errors import RoomError
 from synapse.streams.config import PaginationConfig
+from synapse.events.utils import serialize_event
 from synapse.events.validator import EventValidator
 from synapse.util.logcontext import PreserveLoggingContext
+from synapse.types import UserID
 
 from ._base import BaseHandler
 
@@ -67,7 +69,7 @@ class MessageHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def get_messages(self, user_id=None, room_id=None, pagin_config=None,
-                     feedback=False):
+                     feedback=False, as_client_event=True):
         """Get messages in a room.
 
         Args:
@@ -76,6 +78,7 @@ class MessageHandler(BaseHandler):
             pagin_config (synapse.api.streams.PaginationConfig): The pagination
             config rules to apply, if any.
             feedback (bool): True to get compressed feedback with the messages
+            as_client_event (bool): True to get events in client-server format.
         Returns:
             dict: Pagination API results
         """
@@ -88,7 +91,7 @@ class MessageHandler(BaseHandler):
                 yield self.hs.get_event_sources().get_current_token()
             )
 
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         events, next_key = yield data_source.get_pagination_rows(
             user, pagin_config.get_source_config("room"), room_id
@@ -98,8 +101,12 @@ class MessageHandler(BaseHandler):
             "room_key", next_key
         )
 
+        time_now = self.clock.time_msec()
+
         chunk = {
-            "chunk": [self.hs.serialize_event(e) for e in events],
+            "chunk": [
+                serialize_event(e, time_now, as_client_event) for e in events
+            ],
             "start": pagin_config.from_token.to_string(),
             "end": next_token.to_string(),
         }
@@ -127,13 +134,13 @@ class MessageHandler(BaseHandler):
         if ratelimit:
             self.ratelimit(builder.user_id)
         # TODO(paul): Why does 'event' not have a 'user' object?
-        user = self.hs.parse_userid(builder.user_id)
+        user = UserID.from_string(builder.user_id)
         assert self.hs.is_mine(user), "User must be our own: %s" % (user,)
 
         if builder.type == EventTypes.Member:
             membership = builder.content.get("membership", None)
             if membership == Membership.JOIN:
-                joinee = self.hs.parse_userid(builder.state_key)
+                joinee = UserID.from_string(builder.state_key)
                 # If event doesn't include a display name, add one.
                 yield self.distributor.fire(
                     "collect_presencelike_data",
@@ -207,11 +214,12 @@ class MessageHandler(BaseHandler):
 
         # TODO: This is duplicating logic from snapshot_all_rooms
         current_state = yield self.state_handler.get_current_state(room_id)
-        defer.returnValue([self.hs.serialize_event(c) for c in current_state])
+        now = self.clock.time_msec()
+        defer.returnValue([serialize_event(c, now) for c in current_state])
 
     @defer.inlineCallbacks
     def snapshot_all_rooms(self, user_id=None, pagin_config=None,
-                           feedback=False):
+                           feedback=False, as_client_event=True):
         """Retrieve a snapshot of all rooms the user is invited or has joined.
 
         This snapshot may include messages for all rooms where the user is
@@ -222,6 +230,7 @@ class MessageHandler(BaseHandler):
             pagin_config (synapse.api.streams.PaginationConfig): The pagination
             config used to determine how many messages *PER ROOM* to return.
             feedback (bool): True to get feedback along with these messages.
+            as_client_event (bool): True to get events in client-server format.
         Returns:
             A list of dicts with "room_id" and "membership" keys for all rooms
             the user is currently invited or joined in on. Rooms where the user
@@ -233,7 +242,7 @@ class MessageHandler(BaseHandler):
             membership_list=[Membership.INVITE, Membership.JOIN]
         )
 
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         rooms_ret = []
 
@@ -278,9 +287,13 @@ class MessageHandler(BaseHandler):
 
                 start_token = now_token.copy_and_replace("room_key", token[0])
                 end_token = now_token.copy_and_replace("room_key", token[1])
+                time_now = self.clock.time_msec()
 
                 d["messages"] = {
-                    "chunk": [self.hs.serialize_event(m) for m in messages],
+                    "chunk": [
+                        serialize_event(m, time_now, as_client_event)
+                        for m in messages
+                    ],
                     "start": start_token.to_string(),
                     "end": end_token.to_string(),
                 }
@@ -289,7 +302,8 @@ class MessageHandler(BaseHandler):
                     event.room_id
                 )
                 d["state"] = [
-                    self.hs.serialize_event(c) for c in current_state
+                    serialize_event(c, time_now, as_client_event)
+                    for c in current_state
                 ]
             except:
                 logger.exception("Failed to get snapshot")
@@ -309,11 +323,12 @@ class MessageHandler(BaseHandler):
 
         # TODO(paul): I wish I was called with user objects not user_id
         #   strings...
-        auth_user = self.hs.parse_userid(user_id)
+        auth_user = UserID.from_string(user_id)
 
         # TODO: These concurrently
+        time_now = self.clock.time_msec()
         state_tuples = yield self.state_handler.get_current_state(room_id)
-        state = [self.hs.serialize_event(x) for x in state_tuples]
+        state = [serialize_event(x, time_now) for x in state_tuples]
 
         member_event = (yield self.store.get_room_member(
             user_id=user_id,
@@ -342,7 +357,7 @@ class MessageHandler(BaseHandler):
         for m in room_members:
             try:
                 member_presence = yield presence_handler.get_state(
-                    target_user=self.hs.parse_userid(m.user_id),
+                    target_user=UserID.from_string(m.user_id),
                     auth_user=auth_user,
                     as_event=True,
                 )
@@ -352,11 +367,13 @@ class MessageHandler(BaseHandler):
                     "Failed to get member presence of %r", m.user_id
                 )
 
+        time_now = self.clock.time_msec()
+
         defer.returnValue({
             "membership": member_event.membership,
             "room_id": room_id,
             "messages": {
-                "chunk": [self.hs.serialize_event(m) for m in messages],
+                "chunk": [serialize_event(m, time_now) for m in messages],
                 "start": start_token.to_string(),
                 "end": end_token.to_string(),
             },
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index 8aeed99274..cd0798c2b0 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -20,6 +20,7 @@ from synapse.api.constants import PresenceState
 
 from synapse.util.logutils import log_function
 from synapse.util.logcontext import PreserveLoggingContext
+from synapse.types import UserID
 
 from ._base import BaseHandler
 
@@ -86,6 +87,10 @@ class PresenceHandler(BaseHandler):
             "changed_presencelike_data", self.changed_presencelike_data
         )
 
+        # outbound signal from the presence module to advertise when a user's
+        # presence has changed
+        distributor.declare("user_presence_changed")
+
         self.distributor = distributor
 
         self.federation = hs.get_replication_layer()
@@ -96,22 +101,22 @@ class PresenceHandler(BaseHandler):
         self.federation.register_edu_handler(
             "m.presence_invite",
             lambda origin, content: self.invite_presence(
-                observed_user=hs.parse_userid(content["observed_user"]),
-                observer_user=hs.parse_userid(content["observer_user"]),
+                observed_user=UserID.from_string(content["observed_user"]),
+                observer_user=UserID.from_string(content["observer_user"]),
             )
         )
         self.federation.register_edu_handler(
             "m.presence_accept",
             lambda origin, content: self.accept_presence(
-                observed_user=hs.parse_userid(content["observed_user"]),
-                observer_user=hs.parse_userid(content["observer_user"]),
+                observed_user=UserID.from_string(content["observed_user"]),
+                observer_user=UserID.from_string(content["observer_user"]),
             )
         )
         self.federation.register_edu_handler(
             "m.presence_deny",
             lambda origin, content: self.deny_presence(
-                observed_user=hs.parse_userid(content["observed_user"]),
-                observer_user=hs.parse_userid(content["observer_user"]),
+                observed_user=UserID.from_string(content["observed_user"]),
+                observer_user=UserID.from_string(content["observer_user"]),
             )
         )
 
@@ -418,7 +423,7 @@ class PresenceHandler(BaseHandler):
         )
 
         for p in presence:
-            observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
+            observed_user = UserID.from_string(p.pop("observed_user_id"))
             p["observed_user"] = observed_user
             p.update(self._get_or_offline_usercache(observed_user).get_state())
             if "last_active" in p:
@@ -441,7 +446,7 @@ class PresenceHandler(BaseHandler):
                 user.localpart, accepted=True
             )
             target_users = set([
-                self.hs.parse_userid(x["observed_user_id"]) for x in presence
+                UserID.from_string(x["observed_user_id"]) for x in presence
             ])
 
             # Also include people in all my rooms
@@ -603,6 +608,7 @@ class PresenceHandler(BaseHandler):
             room_ids=room_ids,
             statuscache=statuscache,
         )
+        yield self.distributor.fire("user_presence_changed", user, statuscache)
 
     @defer.inlineCallbacks
     def _push_presence_remote(self, user, destination, state=None):
@@ -646,7 +652,7 @@ class PresenceHandler(BaseHandler):
         deferreds = []
 
         for push in content.get("push", []):
-            user = self.hs.parse_userid(push["user_id"])
+            user = UserID.from_string(push["user_id"])
 
             logger.debug("Incoming presence update from %s", user)
 
@@ -694,7 +700,7 @@ class PresenceHandler(BaseHandler):
                 del self._user_cachemap[user]
 
         for poll in content.get("poll", []):
-            user = self.hs.parse_userid(poll)
+            user = UserID.from_string(poll)
 
             if not self.hs.is_mine(user):
                 continue
@@ -709,7 +715,7 @@ class PresenceHandler(BaseHandler):
             deferreds.append(self._push_presence_remote(user, origin))
 
         for unpoll in content.get("unpoll", []):
-            user = self.hs.parse_userid(unpoll)
+            user = UserID.from_string(unpoll)
 
             if not self.hs.is_mine(user):
                 continue
diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py
index 7777d3cc94..03b2159c53 100644
--- a/synapse/handlers/profile.py
+++ b/synapse/handlers/profile.py
@@ -18,6 +18,7 @@ from twisted.internet import defer
 from synapse.api.errors import SynapseError, AuthError, CodeMessageException
 from synapse.api.constants import EventTypes, Membership
 from synapse.util.logcontext import PreserveLoggingContext
+from synapse.types import UserID
 
 from ._base import BaseHandler
 
@@ -169,7 +170,7 @@ class ProfileHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def on_profile_query(self, args):
-        user = self.hs.parse_userid(args["user_id"])
+        user = UserID.from_string(args["user_id"])
         if not self.hs.is_mine(user):
             raise SynapseError(400, "User is not hosted on this Home Server")
 
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 59719a1fae..23821d321f 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -16,12 +16,14 @@
 """Contains functions for performing events on rooms."""
 from twisted.internet import defer
 
+from ._base import BaseHandler
+
 from synapse.types import UserID, RoomAlias, RoomID
 from synapse.api.constants import EventTypes, Membership, JoinRules
 from synapse.api.errors import StoreError, SynapseError
 from synapse.util import stringutils
 from synapse.util.async import run_on_reactor
-from ._base import BaseHandler
+from synapse.events.utils import serialize_event
 
 import logging
 
@@ -64,7 +66,7 @@ class RoomCreationHandler(BaseHandler):
         invite_list = config.get("invite", [])
         for i in invite_list:
             try:
-                self.hs.parse_userid(i)
+                UserID.from_string(i)
             except:
                 raise SynapseError(400, "Invalid user_id: %s" % (i,))
 
@@ -114,7 +116,7 @@ class RoomCreationHandler(BaseHandler):
                 servers=[self.hs.hostname],
             )
 
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
         creation_events = self._create_events_for_new_room(
             user, room_id, is_public=is_public
         )
@@ -246,11 +248,9 @@ class RoomMemberHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def get_room_members(self, room_id):
-        hs = self.hs
-
         users = yield self.store.get_users_in_room(room_id)
 
-        defer.returnValue([hs.parse_userid(u) for u in users])
+        defer.returnValue([UserID.from_string(u) for u in users])
 
     @defer.inlineCallbacks
     def fetch_room_distributions_into(self, room_id, localusers=None,
@@ -295,8 +295,9 @@ class RoomMemberHandler(BaseHandler):
         yield self.auth.check_joined_room(room_id, user_id)
 
         member_list = yield self.store.get_room_members(room_id=room_id)
+        time_now = self.clock.time_msec()
         event_list = [
-            self.hs.serialize_event(entry)
+            serialize_event(entry, time_now)
             for entry in member_list
         ]
         chunk_data = {
@@ -368,7 +369,7 @@ class RoomMemberHandler(BaseHandler):
             )
 
             if prev_state and prev_state.membership == Membership.JOIN:
-                user = self.hs.parse_userid(event.user_id)
+                user = UserID.from_string(event.user_id)
                 self.distributor.fire(
                     "user_left_room", user=user, room_id=event.room_id
                 )
@@ -412,7 +413,7 @@ class RoomMemberHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def _do_join(self, event, context, room_host=None, do_auth=True):
-        joinee = self.hs.parse_userid(event.state_key)
+        joinee = UserID.from_string(event.state_key)
         # room_id = RoomID.from_string(event.room_id, self.hs)
         room_id = event.room_id
 
@@ -425,10 +426,22 @@ class RoomMemberHandler(BaseHandler):
             event.room_id,
             self.hs.hostname
         )
+        if not is_host_in_room:
+            # is *anyone* in the room?
+            room_member_keys = [
+                v for (k, v) in context.current_state.keys() if (
+                    k == "m.room.member"
+                )
+            ]
+            if len(room_member_keys) == 0:
+                # has the room been created so we can join it?
+                create_event = context.current_state.get(("m.room.create", ""))
+                if create_event:
+                    is_host_in_room = True
 
         if is_host_in_room:
             should_do_dance = False
-        elif room_host:
+        elif room_host:  # TODO: Shouldn't this be remote_room_host?
             should_do_dance = True
         else:
             # TODO(markjh): get prev_state from snapshot
@@ -442,7 +455,8 @@ class RoomMemberHandler(BaseHandler):
                 should_do_dance = not self.hs.is_mine(inviter)
                 room_host = inviter.domain
             else:
-                should_do_dance = False
+                # return the same error as join_room_alias does
+                raise SynapseError(404, "No known servers")
 
         if should_do_dance:
             handler = self.hs.get_handlers().federation_handler
@@ -463,7 +477,7 @@ class RoomMemberHandler(BaseHandler):
                 do_auth=do_auth,
             )
 
-        user = self.hs.parse_userid(event.user_id)
+        user = UserID.from_string(event.user_id)
         yield self.distributor.fire(
             "user_joined_room", user=user, room_id=room_id
         )
@@ -513,7 +527,7 @@ class RoomMemberHandler(BaseHandler):
                                     do_auth):
         yield run_on_reactor()
 
-        target_user = self.hs.parse_userid(event.state_key)
+        target_user = UserID.from_string(event.state_key)
 
         yield self.handle_new_client_event(
             event,
diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py
index ab698b36e1..c69787005f 100644
--- a/synapse/handlers/typing.py
+++ b/synapse/handlers/typing.py
@@ -18,6 +18,7 @@ from twisted.internet import defer
 from ._base import BaseHandler
 
 from synapse.api.errors import SynapseError, AuthError
+from synapse.types import UserID
 
 import logging
 
@@ -83,9 +84,15 @@ class TypingNotificationHandler(BaseHandler):
         if member in self._member_typing_timer:
             self.clock.cancel_call_later(self._member_typing_timer[member])
 
+        def _cb():
+            logger.debug(
+                "%s has timed out in %s", target_user.to_string(), room_id
+            )
+            self._stopped_typing(member)
+
         self._member_typing_until[member] = until
         self._member_typing_timer[member] = self.clock.call_later(
-            timeout / 1000, lambda: self._stopped_typing(member)
+            timeout / 1000.0, _cb
         )
 
         if was_present:
@@ -114,6 +121,10 @@ class TypingNotificationHandler(BaseHandler):
 
         member = RoomMember(room_id=room_id, user=target_user)
 
+        if member in self._member_typing_timer:
+            self.clock.cancel_call_later(self._member_typing_timer[member])
+            del self._member_typing_timer[member]
+
         yield self._stopped_typing(member)
 
     @defer.inlineCallbacks
@@ -136,8 +147,10 @@ class TypingNotificationHandler(BaseHandler):
 
         del self._member_typing_until[member]
 
-        self.clock.cancel_call_later(self._member_typing_timer[member])
-        del self._member_typing_timer[member]
+        if member in self._member_typing_timer:
+            # Don't cancel it - either it already expired, or the real
+            # stopped_typing() will cancel it
+            del self._member_typing_timer[member]
 
     @defer.inlineCallbacks
     def _push_update(self, room_id, user, typing):
@@ -173,7 +186,7 @@ class TypingNotificationHandler(BaseHandler):
     @defer.inlineCallbacks
     def _recv_edu(self, origin, content):
         room_id = content["room_id"]
-        user = self.homeserver.parse_userid(content["user_id"])
+        user = UserID.from_string(content["user_id"])
 
         localusers = set()
 
diff --git a/synapse/http/client.py b/synapse/http/client.py
index 7793bab106..198f575cfa 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -63,6 +63,25 @@ class SimpleHttpClient(object):
         defer.returnValue(json.loads(body))
 
     @defer.inlineCallbacks
+    def post_json_get_json(self, uri, post_json):
+        json_str = json.dumps(post_json)
+
+        logger.info("HTTP POST %s -> %s", json_str, uri)
+
+        response = yield self.agent.request(
+            "POST",
+            uri.encode("ascii"),
+            headers=Headers({
+                "Content-Type": ["application/json"]
+            }),
+            bodyProducer=FileBodyProducer(StringIO(json_str))
+        )
+
+        body = yield readBody(response)
+
+        defer.returnValue(json.loads(body))
+
+    @defer.inlineCallbacks
     def get_json(self, uri, args={}):
         """ Get's some json from the given host and path
 
diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py
index aa14782b0f..1dda3ba2c7 100644
--- a/synapse/http/matrixfederationclient.py
+++ b/synapse/http/matrixfederationclient.py
@@ -72,7 +72,6 @@ class MatrixFederationHttpClient(object):
             requests.
     """
 
-
     def __init__(self, hs):
         self.hs = hs
         self.signing_key = hs.config.signing_key[0]
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 8015a22edf..0f6539e1be 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -16,7 +16,7 @@
 
 from synapse.http.agent_name import AGENT_NAME
 from synapse.api.errors import (
-    cs_exception, SynapseError, CodeMessageException
+    cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError
 )
 from synapse.util.logcontext import LoggingContext
 
@@ -139,11 +139,7 @@ class JsonResource(HttpServer, resource.Resource):
                     return
 
             # Huh. No one wanted to handle that? Fiiiiiine. Send 400.
-            self._send_response(
-                request,
-                400,
-                {"error": "Unrecognized request"}
-            )
+            raise UnrecognizedRequestError()
         except CodeMessageException as e:
             if isinstance(e, SynapseError):
                 logger.info("%s SynapseError: %s - %s", request, e.code, e.msg)
diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py
new file mode 100644
index 0000000000..a4eb6c817c
--- /dev/null
+++ b/synapse/http/servlet.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" This module contains base REST classes for constructing REST servlets. """
+
+from synapse.api.errors import SynapseError
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+class RestServlet(object):
+
+    """ A Synapse REST Servlet.
+
+    An implementing class can either provide its own custom 'register' method,
+    or use the automatic pattern handling provided by the base class.
+
+    To use this latter, the implementing class instead provides a `PATTERN`
+    class attribute containing a pre-compiled regular expression. The automatic
+    register method will then use this method to register any of the following
+    instance methods associated with the corresponding HTTP method:
+
+      on_GET
+      on_PUT
+      on_POST
+      on_DELETE
+      on_OPTIONS
+
+    Automatically handles turning CodeMessageExceptions thrown by these methods
+    into the appropriate HTTP response.
+    """
+
+    def register(self, http_server):
+        """ Register this servlet with the given HTTP server. """
+        if hasattr(self, "PATTERN"):
+            pattern = self.PATTERN
+
+            for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"):
+                if hasattr(self, "on_%s" % (method)):
+                    method_handler = getattr(self, "on_%s" % (method))
+                    http_server.register_path(method, pattern, method_handler)
+        else:
+            raise NotImplementedError("RestServlet must register something.")
+
+    @staticmethod
+    def parse_integer(request, name, default=None, required=False):
+        if name in request.args:
+            try:
+                return int(request.args[name][0])
+            except:
+                message = "Query parameter %r must be an integer" % (name,)
+                raise SynapseError(400, message)
+        else:
+            if required:
+                message = "Missing integer query parameter %r" % (name,)
+                raise SynapseError(400, message)
+            else:
+                return default
+
+    @staticmethod
+    def parse_boolean(request, name, default=None, required=False):
+        if name in request.args:
+            try:
+                return {
+                    "true": True,
+                    "false": False,
+                }[request.args[name][0]]
+            except:
+                message = (
+                    "Boolean query parameter %r must be one of"
+                    " ['true', 'false']"
+                ) % (name,)
+                raise SynapseError(400, message)
+        else:
+            if required:
+                message = "Missing boolean query parameter %r" % (name,)
+                raise SynapseError(400, message)
+            else:
+                return default
+
+    @staticmethod
+    def parse_string(request, name, default=None, required=False,
+                     allowed_values=None, param_type="string"):
+        if name in request.args:
+            value = request.args[name][0]
+            if allowed_values is not None and value not in allowed_values:
+                message = "Query parameter %r must be one of [%s]" % (
+                    name, ", ".join(repr(v) for v in allowed_values)
+                )
+                raise SynapseError(message)
+            else:
+                return value
+        else:
+            if required:
+                message = "Missing %s query parameter %r" % (param_type, name)
+                raise SynapseError(400, message)
+            else:
+                return default
diff --git a/synapse/notifier.py b/synapse/notifier.py
index b9d52d0c4c..3aec1d4af2 100644
--- a/synapse/notifier.py
+++ b/synapse/notifier.py
@@ -244,14 +244,14 @@ class Notifier(object):
             )
 
         if timeout:
-            self.clock.call_later(timeout/1000.0, _timeout_listener)
-
             self._register_with_keys(listener)
 
         yield self._check_for_updates(listener)
 
         if not timeout:
             _timeout_listener()
+        else:
+            self.clock.call_later(timeout/1000.0, _timeout_listener)
 
         return
 
diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py
new file mode 100644
index 0000000000..10ac890482
--- /dev/null
+++ b/synapse/push/__init__.py
@@ -0,0 +1,319 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from synapse.streams.config import PaginationConfig
+from synapse.types import StreamToken
+
+import synapse.util.async
+
+import logging
+import fnmatch
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class Pusher(object):
+    INITIAL_BACKOFF = 1000
+    MAX_BACKOFF = 60 * 60 * 1000
+    GIVE_UP_AFTER = 24 * 60 * 60 * 1000
+    DEFAULT_ACTIONS = ['notify']
+
+    def __init__(self, _hs, instance_handle, user_name, app_id,
+                 app_display_name, device_display_name, pushkey, pushkey_ts,
+                 data, last_token, last_success, failing_since):
+        self.hs = _hs
+        self.evStreamHandler = self.hs.get_handlers().event_stream_handler
+        self.store = self.hs.get_datastore()
+        self.clock = self.hs.get_clock()
+        self.instance_handle = instance_handle
+        self.user_name = user_name
+        self.app_id = app_id
+        self.app_display_name = app_display_name
+        self.device_display_name = device_display_name
+        self.pushkey = pushkey
+        self.pushkey_ts = pushkey_ts
+        self.data = data
+        self.last_token = last_token
+        self.last_success = last_success  # not actually used
+        self.backoff_delay = Pusher.INITIAL_BACKOFF
+        self.failing_since = failing_since
+        self.alive = True
+
+        # The last value of last_active_time that we saw
+        self.last_last_active_time = 0
+
+    @defer.inlineCallbacks
+    def _actions_for_event(self, ev):
+        """
+        This should take into account notification settings that the user
+        has configured both globally and per-room when we have the ability
+        to do such things.
+        """
+        if ev['user_id'] == self.user_name:
+            # let's assume you probably know about messages you sent yourself
+            defer.returnValue(['dont_notify'])
+
+        if ev['type'] == 'm.room.member':
+            if ev['state_key'] != self.user_name:
+                defer.returnValue(['dont_notify'])
+
+        rules = yield self.store.get_push_rules_for_user_name(self.user_name)
+
+        for r in rules:
+            matches = True
+
+            conditions = json.loads(r['conditions'])
+            actions = json.loads(r['actions'])
+
+            for c in conditions:
+                matches &= self._event_fulfills_condition(ev, c)
+            # ignore rules with no actions (we have an explict 'dont_notify'
+            if len(actions) == 0:
+                logger.warn(
+                    "Ignoring rule id %s with no actions for user %s" %
+                    (r['rule_id'], r['user_name'])
+                )
+                continue
+            if matches:
+                defer.returnValue(actions)
+
+        defer.returnValue(Pusher.DEFAULT_ACTIONS)
+
+    def _event_fulfills_condition(self, ev, condition):
+        if condition['kind'] == 'event_match':
+            if 'pattern' not in condition:
+                logger.warn("event_match condition with no pattern")
+                return False
+            pat = condition['pattern']
+
+            val = _value_for_dotted_key(condition['key'], ev)
+            if fnmatch.fnmatch(val, pat):
+                return True
+            return False
+        elif condition['kind'] == 'device':
+            if 'instance_handle' not in condition:
+                return True
+            return condition['instance_handle'] == self.instance_handle
+        else:
+            return True
+
+    @defer.inlineCallbacks
+    def get_context_for_event(self, ev):
+        name_aliases = yield self.store.get_room_name_and_aliases(
+            ev['room_id']
+        )
+
+        ctx = {'aliases': name_aliases[1]}
+        if name_aliases[0] is not None:
+            ctx['name'] = name_aliases[0]
+
+        defer.returnValue(ctx)
+
+    @defer.inlineCallbacks
+    def start(self):
+        if not self.last_token:
+            # First-time setup: get a token to start from (we can't
+            # just start from no token, ie. 'now'
+            # because we need the result to be reproduceable in case
+            # we fail to dispatch the push)
+            config = PaginationConfig(from_token=None, limit='1')
+            chunk = yield self.evStreamHandler.get_stream(
+                self.user_name, config, timeout=0)
+            self.last_token = chunk['end']
+            self.store.update_pusher_last_token(
+                self.user_name, self.pushkey, self.last_token)
+            logger.info("Pusher %s for user %s starting from token %s",
+                        self.pushkey, self.user_name, self.last_token)
+
+        while self.alive:
+            from_tok = StreamToken.from_string(self.last_token)
+            config = PaginationConfig(from_token=from_tok, limit='1')
+            chunk = yield self.evStreamHandler.get_stream(
+                self.user_name, config,
+                timeout=100*365*24*60*60*1000, affect_presence=False
+            )
+
+            # limiting to 1 may get 1 event plus 1 presence event, so
+            # pick out the actual event
+            single_event = None
+            for c in chunk['chunk']:
+                if 'event_id' in c:  # Hmmm...
+                    single_event = c
+                    break
+            if not single_event:
+                self.last_token = chunk['end']
+                continue
+
+            if not self.alive:
+                continue
+
+            processed = False
+            actions = yield self._actions_for_event(single_event)
+            tweaks = _tweaks_for_actions(actions)
+
+            if len(actions) == 0:
+                logger.warn("Empty actions! Using default action.")
+                actions = Pusher.DEFAULT_ACTIONS
+            if 'notify' not in actions and 'dont_notify' not in actions:
+                logger.warn("Neither notify nor dont_notify in actions: adding default")
+                actions.extend(Pusher.DEFAULT_ACTIONS)
+            if 'dont_notify' in actions:
+                logger.debug(
+                    "%s for %s: dont_notify",
+                    single_event['event_id'], self.user_name
+                )
+                processed = True
+            else:
+                rejected = yield self.dispatch_push(single_event, tweaks)
+                if isinstance(rejected, list) or isinstance(rejected, tuple):
+                    processed = True
+                    for pk in rejected:
+                        if pk != self.pushkey:
+                            # for sanity, we only remove the pushkey if it
+                            # was the one we actually sent...
+                            logger.warn(
+                                ("Ignoring rejected pushkey %s because we "
+                                "didn't send it"), pk
+                            )
+                        else:
+                            logger.info(
+                                "Pushkey %s was rejected: removing",
+                                pk
+                            )
+                            yield self.hs.get_pusherpool().remove_pusher(
+                                self.app_id, pk
+                            )
+
+            if not self.alive:
+                continue
+
+            if processed:
+                self.backoff_delay = Pusher.INITIAL_BACKOFF
+                self.last_token = chunk['end']
+                self.store.update_pusher_last_token_and_success(
+                    self.user_name,
+                    self.pushkey,
+                    self.last_token,
+                    self.clock.time_msec()
+                )
+                if self.failing_since:
+                    self.failing_since = None
+                    self.store.update_pusher_failing_since(
+                        self.user_name,
+                        self.pushkey,
+                        self.failing_since)
+            else:
+                if not self.failing_since:
+                    self.failing_since = self.clock.time_msec()
+                    self.store.update_pusher_failing_since(
+                        self.user_name,
+                        self.pushkey,
+                        self.failing_since
+                    )
+
+                if (self.failing_since and
+                   self.failing_since <
+                   self.clock.time_msec() - Pusher.GIVE_UP_AFTER):
+                    # we really only give up so that if the URL gets
+                    # fixed, we don't suddenly deliver a load
+                    # of old notifications.
+                    logger.warn("Giving up on a notification to user %s, "
+                                "pushkey %s",
+                                self.user_name, self.pushkey
+                    )
+                    self.backoff_delay = Pusher.INITIAL_BACKOFF
+                    self.last_token = chunk['end']
+                    self.store.update_pusher_last_token(
+                        self.user_name,
+                        self.pushkey,
+                        self.last_token
+                    )
+
+                    self.failing_since = None
+                    self.store.update_pusher_failing_since(
+                        self.user_name,
+                        self.pushkey,
+                        self.failing_since
+                    )
+                else:
+                    logger.warn("Failed to dispatch push for user %s "
+                                "(failing for %dms)."
+                                "Trying again in %dms",
+                                self.user_name,
+                                self.clock.time_msec() - self.failing_since,
+                                self.backoff_delay
+                    )
+                    yield synapse.util.async.sleep(self.backoff_delay / 1000.0)
+                    self.backoff_delay *= 2
+                    if self.backoff_delay > Pusher.MAX_BACKOFF:
+                        self.backoff_delay = Pusher.MAX_BACKOFF
+
+    def stop(self):
+        self.alive = False
+
+    def dispatch_push(self, p, tweaks):
+        """
+        Overridden by implementing classes to actually deliver the notification
+        Args:
+            p: The event to notify for as a single event from the event stream
+        Returns: If the notification was delivered, an array containing any
+                 pushkeys that were rejected by the push gateway.
+                 False if the notification could not be delivered (ie.
+                 should be retried).
+        """
+        pass
+
+    def reset_badge_count(self):
+        pass
+
+    def presence_changed(self, state):
+        """
+        We clear badge counts whenever a user's last_active time is bumped
+        This is by no means perfect but I think it's the best we can do
+        without read receipts.
+        """
+        if 'last_active' in state.state:
+            last_active = state.state['last_active']
+            if last_active > self.last_last_active_time:
+                logger.info("Resetting badge count for %s", self.user_name)
+                self.reset_badge_count()
+                self.last_last_active_time = last_active
+
+
+def _value_for_dotted_key(dotted_key, event):
+    parts = dotted_key.split(".")
+    val = event
+    while len(parts) > 0:
+        if parts[0] not in val:
+            return None
+        val = val[parts[0]]
+        parts = parts[1:]
+    return val
+
+def _tweaks_for_actions(actions):
+    tweaks = {}
+    for a in actions:
+        if not isinstance(a, dict):
+            continue
+        if 'set_sound' in a:
+            tweaks['sound'] = a['set_sound']
+    return tweaks
+
+class PusherConfigException(Exception):
+    def __init__(self, msg):
+        super(PusherConfigException, self).__init__(msg)
\ No newline at end of file
diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py
new file mode 100644
index 0000000000..e12b946727
--- /dev/null
+++ b/synapse/push/httppusher.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synapse.push import Pusher, PusherConfigException
+from synapse.http.client import SimpleHttpClient
+
+from twisted.internet import defer
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class HttpPusher(Pusher):
+    def __init__(self, _hs, instance_handle, user_name, app_id,
+                 app_display_name, device_display_name, pushkey, pushkey_ts,
+                 data, last_token, last_success, failing_since):
+        super(HttpPusher, self).__init__(
+            _hs,
+            instance_handle,
+            user_name,
+            app_id,
+            app_display_name,
+            device_display_name,
+            pushkey,
+            pushkey_ts,
+            data,
+            last_token,
+            last_success,
+            failing_since
+        )
+        if 'url' not in data:
+            raise PusherConfigException(
+                "'url' required in data for HTTP pusher"
+            )
+        self.url = data['url']
+        self.httpCli = SimpleHttpClient(self.hs)
+        self.data_minus_url = {}
+        self.data_minus_url.update(self.data)
+        del self.data_minus_url['url']
+
+    @defer.inlineCallbacks
+    def _build_notification_dict(self, event, tweaks):
+        # we probably do not want to push for every presence update
+        # (we may want to be able to set up notifications when specific
+        # people sign in, but we'd want to only deliver the pertinent ones)
+        # Actually, presence events will not get this far now because we
+        # need to filter them out in the main Pusher code.
+        if 'event_id' not in event:
+            defer.returnValue(None)
+
+        ctx = yield self.get_context_for_event(event)
+
+        d = {
+            'notification': {
+                'id': event['event_id'],
+                'type': event['type'],
+                'from': event['user_id'],
+                # we may have to fetch this over federation and we
+                # can't trust it anyway: is it worth it?
+                #'from_display_name': 'Steve Stevington'
+                'counts': { #-- we don't mark messages as read yet so
+                # we have no way of knowing
+                    # Just set the badge to 1 until we have read receipts
+                    'unread': 1,
+                #    'missed_calls': 2
+                },
+                'devices': [
+                    {
+                        'app_id': self.app_id,
+                        'pushkey': self.pushkey,
+                        'pushkey_ts': long(self.pushkey_ts / 1000),
+                        'data': self.data_minus_url,
+                        'tweaks': tweaks
+                    }
+                ]
+            }
+        }
+        if event['type'] == 'm.room.member':
+            d['notification']['membership'] = event['content']['membership']
+
+        if len(ctx['aliases']):
+            d['notification']['room_alias'] = ctx['aliases'][0]
+        if 'name' in ctx:
+            d['notification']['room_name'] = ctx['name']
+
+        defer.returnValue(d)
+
+    @defer.inlineCallbacks
+    def dispatch_push(self, event, tweaks):
+        notification_dict = yield self._build_notification_dict(event, tweaks)
+        if not notification_dict:
+            defer.returnValue([])
+        try:
+            resp = yield self.httpCli.post_json_get_json(self.url, notification_dict)
+        except:
+            logger.exception("Failed to push %s ", self.url)
+            defer.returnValue(False)
+        rejected = []
+        if 'rejected' in resp:
+            rejected = resp['rejected']
+        defer.returnValue(rejected)
+
+    @defer.inlineCallbacks
+    def reset_badge_count(self):
+        d = {
+            'notification': {
+                'id': '',
+                'type': None,
+                'from': '',
+                'counts': {
+                    'unread': 0,
+                    'missed_calls': 0
+                },
+                'devices': [
+                    {
+                        'app_id': self.app_id,
+                        'pushkey': self.pushkey,
+                        'pushkey_ts': long(self.pushkey_ts / 1000),
+                        'data': self.data_minus_url,
+                    }
+                ]
+            }
+        }
+        try:
+            resp = yield self.httpCli.post_json_get_json(self.url, d)
+        except:
+            logger.exception("Failed to push %s ", self.url)
+            defer.returnValue(False)
+        rejected = []
+        if 'rejected' in resp:
+            rejected = resp['rejected']
+        defer.returnValue(rejected)
\ No newline at end of file
diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py
new file mode 100644
index 0000000000..856defedac
--- /dev/null
+++ b/synapse/push/pusherpool.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from httppusher import HttpPusher
+from synapse.push import PusherConfigException
+
+import logging
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class PusherPool:
+    def __init__(self, _hs):
+        self.hs = _hs
+        self.store = self.hs.get_datastore()
+        self.pushers = {}
+        self.last_pusher_started = -1
+
+        distributor = self.hs.get_distributor()
+        distributor.observe(
+            "user_presence_changed", self.user_presence_changed
+        )
+
+    @defer.inlineCallbacks
+    def user_presence_changed(self, user, state):
+        user_name = user.to_string()
+
+        # until we have read receipts, pushers use this to reset a user's
+        # badge counters to zero
+        for p in self.pushers.values():
+            if p.user_name == user_name:
+                yield p.presence_changed(state)
+
+    @defer.inlineCallbacks
+    def start(self):
+        pushers = yield self.store.get_all_pushers()
+        for p in pushers:
+            p['data'] = json.loads(p['data'])
+        self._start_pushers(pushers)
+
+    @defer.inlineCallbacks
+    def add_pusher(self, user_name, instance_handle, kind, app_id,
+                   app_display_name, device_display_name, pushkey, lang, data):
+        # we try to create the pusher just to validate the config: it
+        # will then get pulled out of the database,
+        # recreated, added and started: this means we have only one
+        # code path adding pushers.
+        self._create_pusher({
+            "user_name": user_name,
+            "kind": kind,
+            "instance_handle": instance_handle,
+            "app_id": app_id,
+            "app_display_name": app_display_name,
+            "device_display_name": device_display_name,
+            "pushkey": pushkey,
+            "pushkey_ts": self.hs.get_clock().time_msec(),
+            "lang": lang,
+            "data": data,
+            "last_token": None,
+            "last_success": None,
+            "failing_since": None
+        })
+        yield self._add_pusher_to_store(
+            user_name, instance_handle, kind, app_id,
+            app_display_name, device_display_name,
+            pushkey, lang, data
+        )
+
+    @defer.inlineCallbacks
+    def _add_pusher_to_store(self, user_name, instance_handle, kind, app_id,
+                             app_display_name, device_display_name,
+                             pushkey, lang, data):
+        yield self.store.add_pusher(
+            user_name=user_name,
+            instance_handle=instance_handle,
+            kind=kind,
+            app_id=app_id,
+            app_display_name=app_display_name,
+            device_display_name=device_display_name,
+            pushkey=pushkey,
+            pushkey_ts=self.hs.get_clock().time_msec(),
+            lang=lang,
+            data=json.dumps(data)
+        )
+        self._refresh_pusher((app_id, pushkey))
+
+    def _create_pusher(self, pusherdict):
+        if pusherdict['kind'] == 'http':
+            return HttpPusher(
+                self.hs,
+                instance_handle=pusherdict['instance_handle'],
+                user_name=pusherdict['user_name'],
+                app_id=pusherdict['app_id'],
+                app_display_name=pusherdict['app_display_name'],
+                device_display_name=pusherdict['device_display_name'],
+                pushkey=pusherdict['pushkey'],
+                pushkey_ts=pusherdict['pushkey_ts'],
+                data=pusherdict['data'],
+                last_token=pusherdict['last_token'],
+                last_success=pusherdict['last_success'],
+                failing_since=pusherdict['failing_since']
+            )
+        else:
+            raise PusherConfigException(
+                "Unknown pusher type '%s' for user %s" %
+                (pusherdict['kind'], pusherdict['user_name'])
+            )
+
+    @defer.inlineCallbacks
+    def _refresh_pusher(self, app_id_pushkey):
+        p = yield self.store.get_pushers_by_app_id_and_pushkey(
+            app_id_pushkey
+        )
+        p['data'] = json.loads(p['data'])
+
+        self._start_pushers([p])
+
+    def _start_pushers(self, pushers):
+        logger.info("Starting %d pushers", len(pushers))
+        for pusherdict in pushers:
+            p = self._create_pusher(pusherdict)
+            if p:
+                fullid = "%s:%s" % (pusherdict['app_id'], pusherdict['pushkey'])
+                if fullid in self.pushers:
+                    self.pushers[fullid].stop()
+                self.pushers[fullid] = p
+                p.start()
+
+    @defer.inlineCallbacks
+    def remove_pusher(self, app_id, pushkey):
+        fullid = "%s:%s" % (app_id, pushkey)
+        if fullid in self.pushers:
+            logger.info("Stopping pusher %s", fullid)
+            self.pushers[fullid].stop()
+            del self.pushers[fullid]
+        yield self.store.delete_pusher_by_app_id_pushkey(app_id, pushkey)
\ No newline at end of file
diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py
new file mode 100644
index 0000000000..4182ad990f
--- /dev/null
+++ b/synapse/python_dependencies.py
@@ -0,0 +1,80 @@
+import logging
+from distutils.version import LooseVersion
+
+logger = logging.getLogger(__name__)
+
+REQUIREMENTS = {
+    "syutil==0.0.2": ["syutil"],
+    "matrix_angular_sdk==0.6.0": ["syweb>=0.6.0"],
+    "Twisted>=14.0.0": ["twisted>=14.0.0"],
+    "service_identity>=1.0.0": ["service_identity>=1.0.0"],
+    "pyopenssl>=0.14": ["OpenSSL>=0.14"],
+    "pyyaml": ["yaml"],
+    "pyasn1": ["pyasn1"],
+    "pynacl": ["nacl"],
+    "daemonize": ["daemonize"],
+    "py-bcrypt": ["bcrypt"],
+    "frozendict>=0.4": ["frozendict"],
+    "pillow": ["PIL"],
+}
+
+
+class MissingRequirementError(Exception):
+    pass
+
+
+def check_requirements():
+    """Checks that all the modules needed by synapse have been correctly
+    installed and are at the correct version"""
+    for dependency, module_requirements in REQUIREMENTS.items():
+        for module_requirement in module_requirements:
+            if ">=" in module_requirement:
+                module_name, required_version = module_requirement.split(">=")
+                version_test = ">="
+            elif "==" in module_requirement:
+                module_name, required_version = module_requirement.split("==")
+                version_test = "=="
+            else:
+                module_name = module_requirement
+                version_test = None
+
+            try:
+                module = __import__(module_name)
+            except ImportError:
+                logging.exception(
+                    "Can't import %r which is part of %r",
+                    module_name, dependency
+                )
+                raise MissingRequirementError(
+                    "Can't import %r which is part of %r"
+                    % (module_name, dependency)
+                )
+            version = getattr(module, "__version__", None)
+            file_path = getattr(module, "__file__", None)
+            logger.info(
+                "Using %r version %r from %r to satisfy %r",
+                module_name, version, file_path, dependency
+            )
+
+            if version_test == ">=":
+                if version is None:
+                    raise MissingRequirementError(
+                        "Version of %r isn't set as __version__ of module %r"
+                        % (dependency, module_name)
+                    )
+                if LooseVersion(version) < LooseVersion(required_version):
+                    raise MissingRequirementError(
+                        "Version of %r in %r is too old. %r < %r"
+                        % (dependency, file_path, version, required_version)
+                    )
+            elif version_test == "==":
+                if version is None:
+                    raise MissingRequirementError(
+                        "Version of %r isn't set as __version__ of module %r"
+                        % (dependency, module_name)
+                    )
+                if LooseVersion(version) != LooseVersion(required_version):
+                    raise MissingRequirementError(
+                        "Unexpected version of %r in %r. %r != %r"
+                        % (dependency, file_path, version, required_version)
+                    )
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index 88ec9cd27d..90afd93333 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014, 2015 OpenMarket Ltd
+# Copyright 2015 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,37 +11,4 @@
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-from . import (
-    room, events, register, login, profile, presence, initial_sync, directory,
-    voip, admin,
-)
-
-
-class RestServletFactory(object):
-
-    """ A factory for creating REST servlets.
-
-    These REST servlets represent the entire client-server REST API. Generally
-    speaking, they serve as wrappers around events and the handlers that
-    process them.
-
-    See synapse.events for information on synapse events.
-    """
-
-    def __init__(self, hs):
-        client_resource = hs.get_resource_for_client()
-
-        # TODO(erikj): There *must* be a better way of doing this.
-        room.register_servlets(hs, client_resource)
-        events.register_servlets(hs, client_resource)
-        register.register_servlets(hs, client_resource)
-        login.register_servlets(hs, client_resource)
-        profile.register_servlets(hs, client_resource)
-        presence.register_servlets(hs, client_resource)
-        initial_sync.register_servlets(hs, client_resource)
-        directory.register_servlets(hs, client_resource)
-        voip.register_servlets(hs, client_resource)
-        admin.register_servlets(hs, client_resource)
+# limitations under the License.
\ No newline at end of file
diff --git a/synapse/rest/base.py b/synapse/rest/base.py
deleted file mode 100644
index c583945527..0000000000
--- a/synapse/rest/base.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014, 2015 OpenMarket Ltd
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-""" This module contains base REST classes for constructing REST servlets. """
-from synapse.api.urls import CLIENT_PREFIX
-from synapse.rest.transactions import HttpTransactionStore
-import re
-
-import logging
-
-
-logger = logging.getLogger(__name__)
-
-
-def client_path_pattern(path_regex):
-    """Creates a regex compiled client path with the correct client path
-    prefix.
-
-    Args:
-        path_regex (str): The regex string to match. This should NOT have a ^
-        as this will be prefixed.
-    Returns:
-        SRE_Pattern
-    """
-    return re.compile("^" + CLIENT_PREFIX + path_regex)
-
-
-class RestServlet(object):
-
-    """ A Synapse REST Servlet.
-
-    An implementing class can either provide its own custom 'register' method,
-    or use the automatic pattern handling provided by the base class.
-
-    To use this latter, the implementing class instead provides a `PATTERN`
-    class attribute containing a pre-compiled regular expression. The automatic
-    register method will then use this method to register any of the following
-    instance methods associated with the corresponding HTTP method:
-
-      on_GET
-      on_PUT
-      on_POST
-      on_DELETE
-      on_OPTIONS
-
-    Automatically handles turning CodeMessageExceptions thrown by these methods
-    into the appropriate HTTP response.
-    """
-
-    def __init__(self, hs):
-        self.hs = hs
-
-        self.handlers = hs.get_handlers()
-        self.builder_factory = hs.get_event_builder_factory()
-        self.auth = hs.get_auth()
-        self.txns = HttpTransactionStore()
-
-    def register(self, http_server):
-        """ Register this servlet with the given HTTP server. """
-        if hasattr(self, "PATTERN"):
-            pattern = self.PATTERN
-
-            for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"):
-                if hasattr(self, "on_%s" % (method)):
-                    method_handler = getattr(self, "on_%s" % (method))
-                    http_server.register_path(method, pattern, method_handler)
-        else:
-            raise NotImplementedError("RestServlet must register something.")
diff --git a/synapse/rest/client/__init__.py b/synapse/rest/client/__init__.py
new file mode 100644
index 0000000000..1a84d94cd9
--- /dev/null
+++ b/synapse/rest/client/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py
new file mode 100644
index 0000000000..d8d01cdd16
--- /dev/null
+++ b/synapse/rest/client/v1/__init__.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import (
+    room, events, register, login, profile, presence, initial_sync, directory,
+    voip, admin, pusher, push_rule
+)
+
+from synapse.http.server import JsonResource
+
+
+class ClientV1RestResource(JsonResource):
+    """A resource for version 1 of the matrix client API."""
+
+    def __init__(self, hs):
+        JsonResource.__init__(self)
+        self.register_servlets(self, hs)
+
+    @staticmethod
+    def register_servlets(client_resource, hs):
+        room.register_servlets(hs, client_resource)
+        events.register_servlets(hs, client_resource)
+        register.register_servlets(hs, client_resource)
+        login.register_servlets(hs, client_resource)
+        profile.register_servlets(hs, client_resource)
+        presence.register_servlets(hs, client_resource)
+        initial_sync.register_servlets(hs, client_resource)
+        directory.register_servlets(hs, client_resource)
+        voip.register_servlets(hs, client_resource)
+        admin.register_servlets(hs, client_resource)
+        pusher.register_servlets(hs, client_resource)
+        push_rule.register_servlets(hs, client_resource)
diff --git a/synapse/rest/admin.py b/synapse/rest/client/v1/admin.py
index 0aa83514c8..1051d96f96 100644
--- a/synapse/rest/admin.py
+++ b/synapse/rest/client/v1/admin.py
@@ -16,19 +16,21 @@
 from twisted.internet import defer
 
 from synapse.api.errors import AuthError, SynapseError
-from base import RestServlet, client_path_pattern
+from synapse.types import UserID
+
+from base import ClientV1RestServlet, client_path_pattern
 
 import logging
 
 logger = logging.getLogger(__name__)
 
 
-class WhoisRestServlet(RestServlet):
+class WhoisRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/admin/whois/(?P<user_id>[^/]*)")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
-        target_user = self.hs.parse_userid(user_id)
+        target_user = UserID.from_string(user_id)
         auth_user = yield self.auth.get_user_by_req(request)
         is_admin = yield self.auth.is_server_admin(auth_user)
 
diff --git a/synapse/rest/client/v1/base.py b/synapse/rest/client/v1/base.py
new file mode 100644
index 0000000000..72332bdb10
--- /dev/null
+++ b/synapse/rest/client/v1/base.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module contains base REST classes for constructing client v1 servlets.
+"""
+
+from synapse.http.servlet import RestServlet
+from synapse.api.urls import CLIENT_PREFIX
+from .transactions import HttpTransactionStore
+import re
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+def client_path_pattern(path_regex):
+    """Creates a regex compiled client path with the correct client path
+    prefix.
+
+    Args:
+        path_regex (str): The regex string to match. This should NOT have a ^
+        as this will be prefixed.
+    Returns:
+        SRE_Pattern
+    """
+    return re.compile("^" + CLIENT_PREFIX + path_regex)
+
+
+class ClientV1RestServlet(RestServlet):
+    """A base Synapse REST Servlet for the client version 1 API.
+    """
+
+    def __init__(self, hs):
+        self.hs = hs
+        self.handlers = hs.get_handlers()
+        self.builder_factory = hs.get_event_builder_factory()
+        self.auth = hs.get_auth()
+        self.txns = HttpTransactionStore()
diff --git a/synapse/rest/directory.py b/synapse/rest/client/v1/directory.py
index 7ff44fdd9e..15ae8749b8 100644
--- a/synapse/rest/directory.py
+++ b/synapse/rest/client/v1/directory.py
@@ -17,7 +17,8 @@
 from twisted.internet import defer
 
 from synapse.api.errors import AuthError, SynapseError, Codes
-from base import RestServlet, client_path_pattern
+from synapse.types import RoomAlias
+from .base import ClientV1RestServlet, client_path_pattern
 
 import json
 import logging
@@ -30,12 +31,12 @@ def register_servlets(hs, http_server):
     ClientDirectoryServer(hs).register(http_server)
 
 
-class ClientDirectoryServer(RestServlet):
+class ClientDirectoryServer(ClientV1RestServlet):
     PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$")
 
     @defer.inlineCallbacks
     def on_GET(self, request, room_alias):
-        room_alias = self.hs.parse_roomalias(room_alias)
+        room_alias = RoomAlias.from_string(room_alias)
 
         dir_handler = self.handlers.directory_handler
         res = yield dir_handler.get_association(room_alias)
@@ -53,7 +54,7 @@ class ClientDirectoryServer(RestServlet):
 
         logger.debug("Got content: %s", content)
 
-        room_alias = self.hs.parse_roomalias(room_alias)
+        room_alias = RoomAlias.from_string(room_alias)
 
         logger.debug("Got room name: %s", room_alias.to_string())
 
@@ -92,7 +93,7 @@ class ClientDirectoryServer(RestServlet):
 
         dir_handler = self.handlers.directory_handler
 
-        room_alias = self.hs.parse_roomalias(room_alias)
+        room_alias = RoomAlias.from_string(room_alias)
 
         yield dir_handler.delete_association(
             user.to_string(), room_alias
diff --git a/synapse/rest/events.py b/synapse/rest/client/v1/events.py
index cf6d13f817..a0d051227b 100644
--- a/synapse/rest/events.py
+++ b/synapse/rest/client/v1/events.py
@@ -18,7 +18,8 @@ from twisted.internet import defer
 
 from synapse.api.errors import SynapseError
 from synapse.streams.config import PaginationConfig
-from synapse.rest.base import RestServlet, client_path_pattern
+from .base import ClientV1RestServlet, client_path_pattern
+from synapse.events.utils import serialize_event
 
 import logging
 
@@ -26,7 +27,7 @@ import logging
 logger = logging.getLogger(__name__)
 
 
-class EventStreamRestServlet(RestServlet):
+class EventStreamRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/events$")
 
     DEFAULT_LONGPOLL_TIME_MS = 30000
@@ -44,8 +45,11 @@ class EventStreamRestServlet(RestServlet):
                 except ValueError:
                     raise SynapseError(400, "timeout must be in milliseconds.")
 
+            as_client_event = "raw" not in request.args
+
             chunk = yield handler.get_stream(
-                auth_user.to_string(), pagin_config, timeout=timeout
+                auth_user.to_string(), pagin_config, timeout=timeout,
+                as_client_event=as_client_event
             )
         except:
             logger.exception("Event stream failed")
@@ -58,17 +62,22 @@ class EventStreamRestServlet(RestServlet):
 
 
 # TODO: Unit test gets, with and without auth, with different kinds of events.
-class EventRestServlet(RestServlet):
+class EventRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/events/(?P<event_id>[^/]*)$")
 
+    def __init__(self, hs):
+        super(EventRestServlet, self).__init__(hs)
+        self.clock = hs.get_clock()
+
     @defer.inlineCallbacks
     def on_GET(self, request, event_id):
         auth_user = yield self.auth.get_user_by_req(request)
         handler = self.handlers.event_handler
         event = yield handler.get_event(auth_user, event_id)
 
+        time_now = self.clock.time_msec()
         if event:
-            defer.returnValue((200, self.hs.serialize_event(event)))
+            defer.returnValue((200, serialize_event(event, time_now)))
         else:
             defer.returnValue((404, "Event not found."))
 
diff --git a/synapse/rest/initial_sync.py b/synapse/rest/client/v1/initial_sync.py
index a571589581..357fa845b4 100644
--- a/synapse/rest/initial_sync.py
+++ b/synapse/rest/client/v1/initial_sync.py
@@ -16,23 +16,26 @@
 from twisted.internet import defer
 
 from synapse.streams.config import PaginationConfig
-from base import RestServlet, client_path_pattern
+from base import ClientV1RestServlet, client_path_pattern
 
 
 # TODO: Needs unit testing
-class InitialSyncRestServlet(RestServlet):
+class InitialSyncRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/initialSync$")
 
     @defer.inlineCallbacks
     def on_GET(self, request):
         user = yield self.auth.get_user_by_req(request)
         with_feedback = "feedback" in request.args
+        as_client_event = "raw" not in request.args
         pagination_config = PaginationConfig.from_request(request)
         handler = self.handlers.message_handler
         content = yield handler.snapshot_all_rooms(
             user_id=user.to_string(),
             pagin_config=pagination_config,
-            feedback=with_feedback)
+            feedback=with_feedback,
+            as_client_event=as_client_event
+        )
 
         defer.returnValue((200, content))
 
diff --git a/synapse/rest/login.py b/synapse/rest/client/v1/login.py
index 6b8deff67b..7116ac98e8 100644
--- a/synapse/rest/login.py
+++ b/synapse/rest/client/v1/login.py
@@ -17,12 +17,12 @@ from twisted.internet import defer
 
 from synapse.api.errors import SynapseError
 from synapse.types import UserID
-from base import RestServlet, client_path_pattern
+from base import ClientV1RestServlet, client_path_pattern
 
 import json
 
 
-class LoginRestServlet(RestServlet):
+class LoginRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/login$")
     PASS_TYPE = "m.login.password"
 
@@ -64,7 +64,7 @@ class LoginRestServlet(RestServlet):
         defer.returnValue((200, result))
 
 
-class LoginFallbackRestServlet(RestServlet):
+class LoginFallbackRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/login/fallback$")
 
     def on_GET(self, request):
@@ -73,7 +73,7 @@ class LoginFallbackRestServlet(RestServlet):
         return (200, {})
 
 
-class PasswordResetRestServlet(RestServlet):
+class PasswordResetRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/login/reset")
 
     @defer.inlineCallbacks
diff --git a/synapse/rest/presence.py b/synapse/rest/client/v1/presence.py
index ca4d2d21f0..b6c207e662 100644
--- a/synapse/rest/presence.py
+++ b/synapse/rest/client/v1/presence.py
@@ -18,7 +18,8 @@
 from twisted.internet import defer
 
 from synapse.api.errors import SynapseError
-from base import RestServlet, client_path_pattern
+from synapse.types import UserID
+from .base import ClientV1RestServlet, client_path_pattern
 
 import json
 import logging
@@ -26,13 +27,13 @@ import logging
 logger = logging.getLogger(__name__)
 
 
-class PresenceStatusRestServlet(RestServlet):
+class PresenceStatusRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/presence/(?P<user_id>[^/]*)/status")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         state = yield self.handlers.presence_handler.get_state(
             target_user=user, auth_user=auth_user)
@@ -42,7 +43,7 @@ class PresenceStatusRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         state = {}
         try:
@@ -71,13 +72,13 @@ class PresenceStatusRestServlet(RestServlet):
         return (200, {})
 
 
-class PresenceListRestServlet(RestServlet):
+class PresenceListRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         if not self.hs.is_mine(user):
             raise SynapseError(400, "User not hosted on this Home Server")
@@ -97,7 +98,7 @@ class PresenceListRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_POST(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         if not self.hs.is_mine(user):
             raise SynapseError(400, "User not hosted on this Home Server")
@@ -118,7 +119,7 @@ class PresenceListRestServlet(RestServlet):
                     raise SynapseError(400, "Bad invite value.")
                 if len(u) == 0:
                     continue
-                invited_user = self.hs.parse_userid(u)
+                invited_user = UserID.from_string(u)
                 yield self.handlers.presence_handler.send_invite(
                     observer_user=user, observed_user=invited_user
                 )
@@ -129,7 +130,7 @@ class PresenceListRestServlet(RestServlet):
                     raise SynapseError(400, "Bad drop value.")
                 if len(u) == 0:
                     continue
-                dropped_user = self.hs.parse_userid(u)
+                dropped_user = UserID.from_string(u)
                 yield self.handlers.presence_handler.drop(
                     observer_user=user, observed_user=dropped_user
                 )
diff --git a/synapse/rest/profile.py b/synapse/rest/client/v1/profile.py
index dc6eb424b0..24f8d56952 100644
--- a/synapse/rest/profile.py
+++ b/synapse/rest/client/v1/profile.py
@@ -16,17 +16,18 @@
 """ This module contains REST servlets to do with profile: /profile/<paths> """
 from twisted.internet import defer
 
-from base import RestServlet, client_path_pattern
+from .base import ClientV1RestServlet, client_path_pattern
+from synapse.types import UserID
 
 import json
 
 
-class ProfileDisplaynameRestServlet(RestServlet):
+class ProfileDisplaynameRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/displayname")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         displayname = yield self.handlers.profile_handler.get_displayname(
             user,
@@ -37,7 +38,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         try:
             content = json.loads(request.content.read())
@@ -54,12 +55,12 @@ class ProfileDisplaynameRestServlet(RestServlet):
         return (200, {})
 
 
-class ProfileAvatarURLRestServlet(RestServlet):
+class ProfileAvatarURLRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/avatar_url")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         avatar_url = yield self.handlers.profile_handler.get_avatar_url(
             user,
@@ -70,7 +71,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         try:
             content = json.loads(request.content.read())
@@ -87,12 +88,12 @@ class ProfileAvatarURLRestServlet(RestServlet):
         return (200, {})
 
 
-class ProfileRestServlet(RestServlet):
+class ProfileRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)")
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         displayname = yield self.handlers.profile_handler.get_displayname(
             user,
diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py
new file mode 100644
index 0000000000..2b33bdac08
--- /dev/null
+++ b/synapse/rest/client/v1/push_rule.py
@@ -0,0 +1,386 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from synapse.api.errors import SynapseError, Codes, UnrecognizedRequestError, NotFoundError, \
+    StoreError
+from .base import ClientV1RestServlet, client_path_pattern
+from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException
+
+import json
+
+
+class PushRuleRestServlet(ClientV1RestServlet):
+    PATTERN = client_path_pattern("/pushrules/.*$")
+    PRIORITY_CLASS_MAP = {
+        'underride': 0,
+        'sender': 1,
+        'room': 2,
+        'content': 3,
+        'override': 4
+    }
+    PRIORITY_CLASS_INVERSE_MAP = {v: k for k,v in PRIORITY_CLASS_MAP.items()}
+    SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = (
+        "Unrecognised request: You probably wanted a trailing slash")
+
+    def rule_spec_from_path(self, path):
+        if len(path) < 2:
+            raise UnrecognizedRequestError()
+        if path[0] != 'pushrules':
+            raise UnrecognizedRequestError()
+
+        scope = path[1]
+        path = path[2:]
+        if scope not in ['global', 'device']:
+            raise UnrecognizedRequestError()
+
+        device = None
+        if scope == 'device':
+            if len(path) == 0:
+                raise UnrecognizedRequestError()
+            device = path[0]
+            path = path[1:]
+
+        if len(path) == 0:
+            raise UnrecognizedRequestError()
+
+        template = path[0]
+        path = path[1:]
+
+        if len(path) == 0:
+            raise UnrecognizedRequestError()
+
+        rule_id = path[0]
+
+        spec = {
+            'scope': scope,
+            'template': template,
+            'rule_id': rule_id
+        }
+        if device:
+            spec['device'] = device
+        return spec
+
+    def rule_tuple_from_request_object(self, rule_template, rule_id, req_obj, device=None):
+        if rule_template in ['override', 'underride']:
+            if 'conditions' not in req_obj:
+                raise InvalidRuleException("Missing 'conditions'")
+            conditions = req_obj['conditions']
+            for c in conditions:
+                if 'kind' not in c:
+                    raise InvalidRuleException("Condition without 'kind'")
+        elif rule_template == 'room':
+            conditions = [{
+                'kind': 'event_match',
+                'key': 'room_id',
+                'pattern': rule_id
+            }]
+        elif rule_template == 'sender':
+            conditions = [{
+                'kind': 'event_match',
+                'key': 'user_id',
+                'pattern': rule_id
+            }]
+        elif rule_template == 'content':
+            if 'pattern' not in req_obj:
+                raise InvalidRuleException("Content rule missing 'pattern'")
+            pat = req_obj['pattern']
+            if pat.strip("*?[]") == pat:
+                # no special glob characters so we assume the user means
+                # 'contains this string' rather than 'is this string'
+                pat = "*%s*" % (pat,)
+            conditions = [{
+                'kind': 'event_match',
+                'key': 'content.body',
+                'pattern': pat
+            }]
+        else:
+            raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
+
+        if device:
+            conditions.append({
+                'kind': 'device',
+                'instance_handle': device
+            })
+
+        if 'actions' not in req_obj:
+            raise InvalidRuleException("No actions found")
+        actions = req_obj['actions']
+
+        for a in actions:
+            if a in ['notify', 'dont_notify', 'coalesce']:
+                pass
+            elif isinstance(a, dict) and 'set_sound' in a:
+                pass
+            else:
+                raise InvalidRuleException("Unrecognised action")
+
+        return conditions, actions
+
+    @defer.inlineCallbacks
+    def on_PUT(self, request):
+        spec = self.rule_spec_from_path(request.postpath)
+        try:
+            priority_class = _priority_class_from_spec(spec)
+        except InvalidRuleException as e:
+            raise SynapseError(400, e.message)
+
+        user = yield self.auth.get_user_by_req(request)
+
+        content = _parse_json(request)
+
+        try:
+            (conditions, actions) = self.rule_tuple_from_request_object(
+                spec['template'],
+                spec['rule_id'],
+                content,
+                device=spec['device'] if 'device' in spec else None
+            )
+        except InvalidRuleException as e:
+            raise SynapseError(400, e.message)
+
+        before = request.args.get("before", None)
+        if before and len(before):
+            before = before[0]
+        after = request.args.get("after", None)
+        if after and len(after):
+            after = after[0]
+
+        try:
+            yield self.hs.get_datastore().add_push_rule(
+                user_name=user.to_string(),
+                rule_id=spec['rule_id'],
+                priority_class=priority_class,
+                conditions=conditions,
+                actions=actions,
+                before=before,
+                after=after
+            )
+        except InconsistentRuleException as e:
+            raise SynapseError(400, e.message)
+        except RuleNotFoundException as e:
+            raise SynapseError(400, e.message)
+
+        defer.returnValue((200, {}))
+
+    @defer.inlineCallbacks
+    def on_DELETE(self, request):
+        spec = self.rule_spec_from_path(request.postpath)
+        try:
+            priority_class = _priority_class_from_spec(spec)
+        except InvalidRuleException as e:
+            raise SynapseError(400, e.message)
+
+        user = yield self.auth.get_user_by_req(request)
+
+        if 'device' in spec:
+            rules = yield self.hs.get_datastore().get_push_rules_for_user_name(
+                user.to_string()
+            )
+
+            for r in rules:
+                conditions = json.loads(r['conditions'])
+                ih = _instance_handle_from_conditions(conditions)
+                if ih == spec['device'] and r['priority_class'] == priority_class:
+                    yield self.hs.get_datastore().delete_push_rule(
+                        user.to_string(), spec['rule_id']
+                    )
+                    defer.returnValue((200, {}))
+            raise NotFoundError()
+        else:
+            try:
+                yield self.hs.get_datastore().delete_push_rule(
+                    user.to_string(), spec['rule_id'],
+                    priority_class=priority_class
+                )
+                defer.returnValue((200, {}))
+            except StoreError as e:
+                if e.code == 404:
+                    raise NotFoundError()
+                else:
+                    raise
+
+    @defer.inlineCallbacks
+    def on_GET(self, request):
+        user = yield self.auth.get_user_by_req(request)
+
+        # we build up the full structure and then decide which bits of it
+        # to send which means doing unnecessary work sometimes but is
+        # is probably not going to make a whole lot of difference
+        rawrules = yield self.hs.get_datastore().get_push_rules_for_user_name(user.to_string())
+
+        rules = {'global': {}, 'device': {}}
+
+        rules['global'] = _add_empty_priority_class_arrays(rules['global'])
+
+        for r in rawrules:
+            rulearray = None
+
+            r["conditions"] = json.loads(r["conditions"])
+            r["actions"] = json.loads(r["actions"])
+
+            template_name = _priority_class_to_template_name(r['priority_class'])
+
+            if r['priority_class'] > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
+                # per-device rule
+                instance_handle = _instance_handle_from_conditions(r["conditions"])
+                r = _strip_device_condition(r)
+                if not instance_handle:
+                    continue
+                if instance_handle not in rules['device']:
+                    rules['device'][instance_handle] = {}
+                    rules['device'][instance_handle] = (
+                        _add_empty_priority_class_arrays(
+                            rules['device'][instance_handle]
+                        )
+                    )
+
+                rulearray = rules['device'][instance_handle][template_name]
+            else:
+                rulearray = rules['global'][template_name]
+
+            template_rule = _rule_to_template(r)
+            if template_rule:
+                rulearray.append(template_rule)
+
+        path = request.postpath[1:]
+
+        if path == []:
+            # we're a reference impl: pedantry is our job.
+            raise UnrecognizedRequestError(PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR)
+
+        if path[0] == '':
+            defer.returnValue((200, rules))
+        elif path[0] == 'global':
+            path = path[1:]
+            result = _filter_ruleset_with_path(rules['global'], path)
+            defer.returnValue((200, result))
+        elif path[0] == 'device':
+            path = path[1:]
+            if path == []:
+                raise UnrecognizedRequestError(PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR)
+            if path[0] == '':
+                defer.returnValue((200, rules['device']))
+
+            instance_handle = path[0]
+            path = path[1:]
+            if instance_handle not in rules['device']:
+                ret = {}
+                ret = _add_empty_priority_class_arrays(ret)
+                defer.returnValue((200, ret))
+            ruleset = rules['device'][instance_handle]
+            result = _filter_ruleset_with_path(ruleset, path)
+            defer.returnValue((200, result))
+        else:
+            raise UnrecognizedRequestError()
+
+    def on_OPTIONS(self, _):
+        return 200, {}
+
+def _add_empty_priority_class_arrays(d):
+    for pc in PushRuleRestServlet.PRIORITY_CLASS_MAP.keys():
+        d[pc] = []
+    return d
+
+def _instance_handle_from_conditions(conditions):
+    """
+    Given a list of conditions, return the instance handle of the
+    device rule if there is one
+    """
+    for c in conditions:
+        if c['kind'] == 'device':
+            return c['instance_handle']
+    return None
+
+def _filter_ruleset_with_path(ruleset, path):
+    if path == []:
+        raise UnrecognizedRequestError(PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR)
+
+    if path[0] == '':
+        return ruleset
+    template_kind = path[0]
+    if template_kind not in ruleset:
+        raise UnrecognizedRequestError()
+    path = path[1:]
+    if path == []:
+        raise UnrecognizedRequestError(PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR)
+    if path[0] == '':
+        return ruleset[template_kind]
+    rule_id = path[0]
+    for r in ruleset[template_kind]:
+        if r['rule_id'] == rule_id:
+            return r
+    raise NotFoundError
+
+def _priority_class_from_spec(spec):
+    if spec['template'] not in PushRuleRestServlet.PRIORITY_CLASS_MAP.keys():
+        raise InvalidRuleException("Unknown template: %s" % (spec['kind']))
+    pc = PushRuleRestServlet.PRIORITY_CLASS_MAP[spec['template']]
+
+    if spec['scope'] == 'device':
+        pc += len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
+
+    return pc
+
+def _priority_class_to_template_name(pc):
+    if pc > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
+        # per-device
+        prio_class_index = pc - len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
+        return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
+    else:
+        return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[pc]
+
+def _rule_to_template(rule):
+    template_name = _priority_class_to_template_name(rule['priority_class'])
+    if template_name in ['override', 'underride']:
+        return {k: rule[k] for k in ["rule_id", "conditions", "actions"]}
+    elif template_name in ["sender", "room"]:
+        return {k: rule[k] for k in ["rule_id", "actions"]}
+    elif template_name == 'content':
+        if len(rule["conditions"]) != 1:
+            return None
+        thecond = rule["conditions"][0]
+        if "pattern" not in thecond:
+            return None
+        ret = {k: rule[k] for k in ["rule_id", "actions"]}
+        ret["pattern"] = thecond["pattern"]
+        return ret
+
+def _strip_device_condition(rule):
+    for i,c in enumerate(rule['conditions']):
+        if c['kind'] == 'device':
+            del rule['conditions'][i]
+    return rule
+
+
+class InvalidRuleException(Exception):
+    pass
+
+
+# XXX: C+ped from rest/room.py - surely this should be common?
+def _parse_json(request):
+    try:
+        content = json.loads(request.content.read())
+        if type(content) != dict:
+            raise SynapseError(400, "Content must be a JSON object.",
+                               errcode=Codes.NOT_JSON)
+        return content
+    except ValueError:
+        raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
+
+
+def register_servlets(hs, http_server):
+    PushRuleRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py
new file mode 100644
index 0000000000..80a11890a3
--- /dev/null
+++ b/synapse/rest/client/v1/pusher.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from synapse.api.errors import SynapseError, Codes
+from synapse.push import PusherConfigException
+from .base import ClientV1RestServlet, client_path_pattern
+
+import json
+
+
+class PusherRestServlet(ClientV1RestServlet):
+    PATTERN = client_path_pattern("/pushers/set$")
+
+    @defer.inlineCallbacks
+    def on_POST(self, request):
+        user = yield self.auth.get_user_by_req(request)
+
+        content = _parse_json(request)
+
+        reqd = ['instance_handle', 'kind', 'app_id', 'app_display_name',
+                'device_display_name', 'pushkey', 'lang', 'data']
+        missing = []
+        for i in reqd:
+            if i not in content:
+                missing.append(i)
+        if len(missing):
+            raise SynapseError(400, "Missing parameters: "+','.join(missing),
+                               errcode=Codes.MISSING_PARAM)
+
+        pusher_pool = self.hs.get_pusherpool()
+        try:
+            yield pusher_pool.add_pusher(
+                user_name=user.to_string(),
+                instance_handle=content['instance_handle'],
+                kind=content['kind'],
+                app_id=content['app_id'],
+                app_display_name=content['app_display_name'],
+                device_display_name=content['device_display_name'],
+                pushkey=content['pushkey'],
+                lang=content['lang'],
+                data=content['data']
+            )
+        except PusherConfigException as pce:
+            raise SynapseError(400, "Config Error: "+pce.message,
+                               errcode=Codes.MISSING_PARAM)
+
+        defer.returnValue((200, {}))
+
+    def on_OPTIONS(self, _):
+        return 200, {}
+
+
+# XXX: C+ped from rest/room.py - surely this should be common?
+def _parse_json(request):
+    try:
+        content = json.loads(request.content.read())
+        if type(content) != dict:
+            raise SynapseError(400, "Content must be a JSON object.",
+                               errcode=Codes.NOT_JSON)
+        return content
+    except ValueError:
+        raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
+
+
+def register_servlets(hs, http_server):
+    PusherRestServlet(hs).register(http_server)
diff --git a/synapse/rest/register.py b/synapse/rest/client/v1/register.py
index e3b26902d9..c0423c2d45 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/client/v1/register.py
@@ -18,7 +18,7 @@ from twisted.internet import defer
 
 from synapse.api.errors import SynapseError, Codes
 from synapse.api.constants import LoginType
-from base import RestServlet, client_path_pattern
+from base import ClientV1RestServlet, client_path_pattern
 import synapse.util.stringutils as stringutils
 
 from synapse.util.async import run_on_reactor
@@ -42,7 +42,7 @@ else:
     compare_digest = lambda a, b: a == b
 
 
-class RegisterRestServlet(RestServlet):
+class RegisterRestServlet(ClientV1RestServlet):
     """Handles registration with the home server.
 
     This servlet is in control of the registration flow; the registration
diff --git a/synapse/rest/room.py b/synapse/rest/client/v1/room.py
index e40773758a..58b09b6fc1 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -16,10 +16,12 @@
 """ This module contains REST servlets to do with rooms: /rooms/<paths> """
 from twisted.internet import defer
 
-from base import RestServlet, client_path_pattern
+from base import ClientV1RestServlet, client_path_pattern
 from synapse.api.errors import SynapseError, Codes
 from synapse.streams.config import PaginationConfig
 from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID, RoomAlias
+from synapse.events.utils import serialize_event
 
 import json
 import logging
@@ -29,7 +31,7 @@ import urllib
 logger = logging.getLogger(__name__)
 
 
-class RoomCreateRestServlet(RestServlet):
+class RoomCreateRestServlet(ClientV1RestServlet):
     # No PATTERN; we have custom dispatch rules here
 
     def register(self, http_server):
@@ -93,7 +95,7 @@ class RoomCreateRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing for generic events
-class RoomStateEventRestServlet(RestServlet):
+class RoomStateEventRestServlet(ClientV1RestServlet):
     def register(self, http_server):
         # /room/$roomid/state/$eventtype
         no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
@@ -162,7 +164,7 @@ class RoomStateEventRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing for generic events + feedback
-class RoomSendEventRestServlet(RestServlet):
+class RoomSendEventRestServlet(ClientV1RestServlet):
 
     def register(self, http_server):
         # /rooms/$roomid/send/$event_type[/$txn_id]
@@ -205,7 +207,7 @@ class RoomSendEventRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing for room ID + alias joins
-class JoinRoomAliasServlet(RestServlet):
+class JoinRoomAliasServlet(ClientV1RestServlet):
 
     def register(self, http_server):
         # /join/$room_identifier[/$txn_id]
@@ -223,10 +225,10 @@ class JoinRoomAliasServlet(RestServlet):
         identifier = None
         is_room_alias = False
         try:
-            identifier = self.hs.parse_roomalias(room_identifier)
+            identifier = RoomAlias.from_string(room_identifier)
             is_room_alias = True
         except SynapseError:
-            identifier = self.hs.parse_roomid(room_identifier)
+            identifier = RoomID.from_string(room_identifier)
 
         # TODO: Support for specifying the home server to join with?
 
@@ -246,7 +248,7 @@ class JoinRoomAliasServlet(RestServlet):
                 }
             )
 
-            defer.returnValue((200, {}))
+            defer.returnValue((200, {"room_id": identifier.to_string()}))
 
     @defer.inlineCallbacks
     def on_PUT(self, request, room_identifier, txn_id):
@@ -264,7 +266,7 @@ class JoinRoomAliasServlet(RestServlet):
 
 
 # TODO: Needs unit testing
-class PublicRoomListRestServlet(RestServlet):
+class PublicRoomListRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/publicRooms$")
 
     @defer.inlineCallbacks
@@ -275,7 +277,7 @@ class PublicRoomListRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing
-class RoomMemberListRestServlet(RestServlet):
+class RoomMemberListRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$")
 
     @defer.inlineCallbacks
@@ -289,7 +291,7 @@ class RoomMemberListRestServlet(RestServlet):
 
         for event in members["chunk"]:
             # FIXME: should probably be state_key here, not user_id
-            target_user = self.hs.parse_userid(event["user_id"])
+            target_user = UserID.from_string(event["user_id"])
             # Presence is an optional cache; don't fail if we can't fetch it
             try:
                 presence_handler = self.handlers.presence_handler
@@ -304,7 +306,7 @@ class RoomMemberListRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing
-class RoomMessageListRestServlet(RestServlet):
+class RoomMessageListRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$")
 
     @defer.inlineCallbacks
@@ -314,18 +316,21 @@ class RoomMessageListRestServlet(RestServlet):
             request, default_limit=10,
         )
         with_feedback = "feedback" in request.args
+        as_client_event = "raw" not in request.args
         handler = self.handlers.message_handler
         msgs = yield handler.get_messages(
             room_id=room_id,
             user_id=user.to_string(),
             pagin_config=pagination_config,
-            feedback=with_feedback)
+            feedback=with_feedback,
+            as_client_event=as_client_event
+        )
 
         defer.returnValue((200, msgs))
 
 
 # TODO: Needs unit testing
-class RoomStateRestServlet(RestServlet):
+class RoomStateRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$")
 
     @defer.inlineCallbacks
@@ -341,7 +346,7 @@ class RoomStateRestServlet(RestServlet):
 
 
 # TODO: Needs unit testing
-class RoomInitialSyncRestServlet(RestServlet):
+class RoomInitialSyncRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$")
 
     @defer.inlineCallbacks
@@ -356,9 +361,13 @@ class RoomInitialSyncRestServlet(RestServlet):
         defer.returnValue((200, content))
 
 
-class RoomTriggerBackfill(RestServlet):
+class RoomTriggerBackfill(ClientV1RestServlet):
     PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$")
 
+    def __init__(self, hs):
+        super(RoomTriggerBackfill, self).__init__(hs)
+        self.clock = hs.get_clock()
+
     @defer.inlineCallbacks
     def on_GET(self, request, room_id):
         remote_server = urllib.unquote(
@@ -370,12 +379,14 @@ class RoomTriggerBackfill(RestServlet):
         handler = self.handlers.federation_handler
         events = yield handler.backfill(remote_server, room_id, limit)
 
-        res = [self.hs.serialize_event(event) for event in events]
+        time_now = self.clock.time_msec()
+
+        res = [serialize_event(event, time_now) for event in events]
         defer.returnValue((200, res))
 
 
 # TODO: Needs unit testing
-class RoomMembershipRestServlet(RestServlet):
+class RoomMembershipRestServlet(ClientV1RestServlet):
 
     def register(self, http_server):
         # /rooms/$roomid/[invite|join|leave]
@@ -427,7 +438,7 @@ class RoomMembershipRestServlet(RestServlet):
         defer.returnValue(response)
 
 
-class RoomRedactEventRestServlet(RestServlet):
+class RoomRedactEventRestServlet(ClientV1RestServlet):
     def register(self, http_server):
         PATTERN = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
         register_txn_path(self, PATTERN, http_server)
@@ -465,7 +476,7 @@ class RoomRedactEventRestServlet(RestServlet):
         defer.returnValue(response)
 
 
-class RoomTypingRestServlet(RestServlet):
+class RoomTypingRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern(
         "/rooms/(?P<room_id>[^/]*)/typing/(?P<user_id>[^/]*)$"
     )
@@ -475,7 +486,7 @@ class RoomTypingRestServlet(RestServlet):
         auth_user = yield self.auth.get_user_by_req(request)
 
         room_id = urllib.unquote(room_id)
-        target_user = self.hs.parse_userid(urllib.unquote(user_id))
+        target_user = UserID.from_string(urllib.unquote(user_id))
 
         content = _parse_json(request)
 
diff --git a/synapse/rest/transactions.py b/synapse/rest/client/v1/transactions.py
index d933fea18a..d933fea18a 100644
--- a/synapse/rest/transactions.py
+++ b/synapse/rest/client/v1/transactions.py
diff --git a/synapse/rest/voip.py b/synapse/rest/client/v1/voip.py
index 011c35e69b..822d863ce6 100644
--- a/synapse/rest/voip.py
+++ b/synapse/rest/client/v1/voip.py
@@ -15,7 +15,7 @@
 
 from twisted.internet import defer
 
-from base import RestServlet, client_path_pattern
+from base import ClientV1RestServlet, client_path_pattern
 
 
 import hmac
@@ -23,7 +23,7 @@ import hashlib
 import base64
 
 
-class VoipRestServlet(RestServlet):
+class VoipRestServlet(ClientV1RestServlet):
     PATTERN = client_path_pattern("/voip/turnServer$")
 
     @defer.inlineCallbacks
diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py
new file mode 100644
index 0000000000..bb740e2803
--- /dev/null
+++ b/synapse/rest/client/v2_alpha/__init__.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synapse.http.server import JsonResource
+
+
+class ClientV2AlphaRestResource(JsonResource):
+    """A resource for version 2 alpha of the matrix client API."""
+
+    def __init__(self, hs):
+        JsonResource.__init__(self)
+        self.register_servlets(self, hs)
+
+    @staticmethod
+    def register_servlets(client_resource, hs):
+        pass
diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py
new file mode 100644
index 0000000000..22dc5cb862
--- /dev/null
+++ b/synapse/rest/client/v2_alpha/_base.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module contains base REST classes for constructing client v1 servlets.
+"""
+
+from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX
+import re
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+def client_v2_pattern(path_regex):
+    """Creates a regex compiled client path with the correct client path
+    prefix.
+
+    Args:
+        path_regex (str): The regex string to match. This should NOT have a ^
+        as this will be prefixed.
+    Returns:
+        SRE_Pattern
+    """
+    return re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex)
diff --git a/synapse/media/__init__.py b/synapse/rest/media/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/synapse/media/__init__.py
+++ b/synapse/rest/media/__init__.py
diff --git a/synapse/media/v0/__init__.py b/synapse/rest/media/v0/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/synapse/media/v0/__init__.py
+++ b/synapse/rest/media/v0/__init__.py
diff --git a/synapse/media/v0/content_repository.py b/synapse/rest/media/v0/content_repository.py
index 79ae0e3d74..79ae0e3d74 100644
--- a/synapse/media/v0/content_repository.py
+++ b/synapse/rest/media/v0/content_repository.py
diff --git a/synapse/media/v1/__init__.py b/synapse/rest/media/v1/__init__.py
index 619999d268..d6c6690577 100644
--- a/synapse/media/v1/__init__.py
+++ b/synapse/rest/media/v1/__init__.py
@@ -22,7 +22,8 @@ except IOError as e:
     if str(e).startswith("decoder jpeg not available"):
         raise Exception(
             "FATAL: jpeg codec not supported. Install pillow correctly! "
-            " 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'"
+            " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&"
+            " pip install pillow --user'"
         )
 except Exception:
     # any other exception is fine
@@ -36,7 +37,8 @@ except IOError as e:
     if str(e).startswith("decoder zip not available"):
         raise Exception(
             "FATAL: zip codec not supported. Install pillow correctly! "
-            " 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'"
+            " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&"
+            " pip install pillow --user'"
         )
 except Exception:
     # any other exception is fine
diff --git a/synapse/media/v1/base_resource.py b/synapse/rest/media/v1/base_resource.py
index 688e7376ad..688e7376ad 100644
--- a/synapse/media/v1/base_resource.py
+++ b/synapse/rest/media/v1/base_resource.py
diff --git a/synapse/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py
index c585bb11f7..c585bb11f7 100644
--- a/synapse/media/v1/download_resource.py
+++ b/synapse/rest/media/v1/download_resource.py
diff --git a/synapse/media/v1/filepath.py b/synapse/rest/media/v1/filepath.py
index ed9a58e9d9..ed9a58e9d9 100644
--- a/synapse/media/v1/filepath.py
+++ b/synapse/rest/media/v1/filepath.py
diff --git a/synapse/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py
index 461cc001f1..461cc001f1 100644
--- a/synapse/media/v1/media_repository.py
+++ b/synapse/rest/media/v1/media_repository.py
diff --git a/synapse/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py
index 84f5e3463c..84f5e3463c 100644
--- a/synapse/media/v1/thumbnail_resource.py
+++ b/synapse/rest/media/v1/thumbnail_resource.py
diff --git a/synapse/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py
index bc86efea8f..28404f2b7b 100644
--- a/synapse/media/v1/thumbnailer.py
+++ b/synapse/rest/media/v1/thumbnailer.py
@@ -82,7 +82,7 @@ class Thumbnailer(object):
 
     def save_image(self, output_image, output_type, output_path):
         output_bytes_io = BytesIO()
-        output_image.save(output_bytes_io, self.FORMATS[output_type])
+        output_image.save(output_bytes_io, self.FORMATS[output_type], quality=70)
         output_bytes = output_bytes_io.getvalue()
         with open(output_path, "wb") as output_file:
             output_file.write(output_bytes)
diff --git a/synapse/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py
index b1718a630b..b1718a630b 100644
--- a/synapse/media/v1/upload_resource.py
+++ b/synapse/rest/media/v1/upload_resource.py
diff --git a/synapse/server.py b/synapse/server.py
index c3bf46abbf..f152f0321e 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -20,20 +20,18 @@
 
 # Imports required for the default HomeServer() implementation
 from synapse.federation import initialize_http_replication
-from synapse.events.utils import serialize_event
 from synapse.notifier import Notifier
 from synapse.api.auth import Auth
 from synapse.handlers import Handlers
-from synapse.rest import RestServletFactory
 from synapse.state import StateHandler
 from synapse.storage import DataStore
-from synapse.types import UserID, RoomAlias, RoomID, EventID
 from synapse.util import Clock
 from synapse.util.distributor import Distributor
 from synapse.util.lockutils import LockManager
 from synapse.streams.events import EventSources
 from synapse.api.ratelimiting import Ratelimiter
 from synapse.crypto.keyring import Keyring
+from synapse.push.pusherpool import PusherPool
 from synapse.events.builder import EventBuilderFactory
 
 
@@ -72,6 +70,7 @@ class BaseHomeServer(object):
         'notifier',
         'distributor',
         'resource_for_client',
+        'resource_for_client_v2_alpha',
         'resource_for_federation',
         'resource_for_web_client',
         'resource_for_content_repo',
@@ -80,6 +79,7 @@ class BaseHomeServer(object):
         'event_sources',
         'ratelimiter',
         'keyring',
+        'pusherpool',
         'event_builder_factory',
     ]
 
@@ -125,33 +125,6 @@ class BaseHomeServer(object):
 
         setattr(BaseHomeServer, "get_%s" % (depname), _get)
 
-    # TODO: Why are these parse_ methods so high up along with other globals?
-    # Surely these should be in a util package or in the api package?
-
-    # Other utility methods
-    def parse_userid(self, s):
-        """Parse the string given by 's' as a User ID and return a UserID
-        object."""
-        return UserID.from_string(s)
-
-    def parse_roomalias(self, s):
-        """Parse the string given by 's' as a Room Alias and return a RoomAlias
-        object."""
-        return RoomAlias.from_string(s)
-
-    def parse_roomid(self, s):
-        """Parse the string given by 's' as a Room ID and return a RoomID
-        object."""
-        return RoomID.from_string(s)
-
-    def parse_eventid(self, s):
-        """Parse the string given by 's' as a Event ID and return a EventID
-        object."""
-        return EventID.from_string(s)
-
-    def serialize_event(self, e):
-        return serialize_event(self, e)
-
     def get_ip_from_request(self, request):
         # May be an X-Forwarding-For header depending on config
         ip_addr = request.getClientIP()
@@ -203,9 +176,6 @@ class HomeServer(BaseHomeServer):
     def build_auth(self):
         return Auth(self)
 
-    def build_rest_servlet_factory(self):
-        return RestServletFactory(self)
-
     def build_state_handler(self):
         return StateHandler(self)
 
@@ -230,8 +200,5 @@ class HomeServer(BaseHomeServer):
             hostname=self.hostname,
         )
 
-    def register_servlets(self):
-        """ Register all servlets associated with this HomeServer.
-        """
-        # Simply building the ServletFactory is sufficient to have it register
-        self.get_rest_servlet_factory()
+    def build_pusherpool(self):
+        return PusherPool(self)
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 4beb951b9f..277581b4e2 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -29,6 +29,8 @@ from .stream import StreamStore
 from .transactions import TransactionStore
 from .keys import KeyStore
 from .event_federation import EventFederationStore
+from .pusher import PusherStore
+from .push_rule import PushRuleStore
 from .media_repository import MediaRepositoryStore
 
 from .state import StateStore
@@ -60,13 +62,14 @@ SCHEMAS = [
     "state",
     "event_edges",
     "event_signatures",
+    "pusher",
     "media_repository",
 ]
 
 
 # Remember to update this number every time an incompatible change is made to
 # database schema files, so the users will be informed on server restarts.
-SCHEMA_VERSION = 11
+SCHEMA_VERSION = 12
 
 
 class _RollbackButIsFineException(Exception):
@@ -82,6 +85,8 @@ class DataStore(RoomMemberStore, RoomStore,
                 DirectoryStore, KeyStore, StateStore, SignatureStore,
                 EventFederationStore,
                 MediaRepositoryStore,
+                PusherStore,
+                PushRuleStore
                 ):
 
     def __init__(self, hs):
@@ -382,6 +387,41 @@ class DataStore(RoomMemberStore, RoomStore,
         defer.returnValue(events)
 
     @defer.inlineCallbacks
+    def get_room_name_and_aliases(self, room_id):
+        del_sql = (
+            "SELECT event_id FROM redactions WHERE redacts = e.event_id "
+            "LIMIT 1"
+        )
+
+        sql = (
+            "SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
+            "INNER JOIN current_state_events as c ON e.event_id = c.event_id "
+            "INNER JOIN state_events as s ON e.event_id = s.event_id "
+            "WHERE c.room_id = ? "
+        ) % {
+            "redacted": del_sql,
+        }
+
+        sql += " AND ((s.type = 'm.room.name' AND s.state_key = '')"
+        sql += " OR s.type = 'm.room.aliases')"
+        args = (room_id,)
+
+        results = yield self._execute_and_decode(sql, *args)
+
+        events = yield self._parse_events(results)
+
+        name = None
+        aliases = []
+
+        for e in events:
+            if e.type == 'm.room.name':
+                name = e.content['name']
+            elif e.type == 'm.room.aliases':
+                aliases.extend(e.content['aliases'])
+
+        defer.returnValue((name, aliases))
+
+    @defer.inlineCallbacks
     def _get_min_token(self):
         row = yield self._execute(
             None,
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index f660fc6eaf..4e8bd3faa9 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -193,6 +193,50 @@ class SQLBaseStore(object):
         txn.execute(sql, values.values())
         return txn.lastrowid
 
+    def _simple_upsert(self, table, keyvalues, values):
+        """
+        Args:
+            table (str): The table to upsert into
+            keyvalues (dict): The unique key tables and their new values
+            values (dict): The nonunique columns and their new values
+        Returns: A deferred
+        """
+        return self.runInteraction(
+            "_simple_upsert",
+            self._simple_upsert_txn, table, keyvalues, values
+        )
+
+    def _simple_upsert_txn(self, txn, table, keyvalues, values):
+        # Try to update
+        sql = "UPDATE %s SET %s WHERE %s" % (
+            table,
+            ", ".join("%s = ?" % (k,) for k in values),
+            " AND ".join("%s = ?" % (k,) for k in keyvalues)
+        )
+        sqlargs = values.values() + keyvalues.values()
+        logger.debug(
+            "[SQL] %s Args=%s",
+            sql, sqlargs,
+        )
+
+        txn.execute(sql, sqlargs)
+        if txn.rowcount == 0:
+            # We didn't update and rows so insert a new one
+            allvalues = {}
+            allvalues.update(keyvalues)
+            allvalues.update(values)
+
+            sql = "INSERT INTO %s (%s) VALUES (%s)" % (
+                table,
+                ", ".join(k for k in allvalues),
+                ", ".join("?" for _ in allvalues)
+            )
+            logger.debug(
+                "[SQL] %s Args=%s",
+                sql, keyvalues.values(),
+            )
+            txn.execute(sql, allvalues.values())
+
     def _simple_select_one(self, table, keyvalues, retcols,
                            allow_none=False):
         """Executes a SELECT query on the named table, which is expected to
@@ -344,8 +388,8 @@ class SQLBaseStore(object):
         if updatevalues:
             update_sql = "UPDATE %s SET %s WHERE %s" % (
                 table,
-                ", ".join("%s = ?" % (k) for k in updatevalues),
-                " AND ".join("%s = ?" % (k) for k in keyvalues)
+                ", ".join("%s = ?" % (k,) for k in updatevalues),
+                " AND ".join("%s = ?" % (k,) for k in keyvalues)
             )
 
         def func(txn):
diff --git a/synapse/storage/push_rule.py b/synapse/storage/push_rule.py
new file mode 100644
index 0000000000..0342996ed1
--- /dev/null
+++ b/synapse/storage/push_rule.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+
+from ._base import SQLBaseStore, Table
+from twisted.internet import defer
+
+import logging
+import copy
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class PushRuleStore(SQLBaseStore):
+    @defer.inlineCallbacks
+    def get_push_rules_for_user_name(self, user_name):
+        sql = (
+            "SELECT "+",".join(PushRuleTable.fields)+" "
+            "FROM "+PushRuleTable.table_name+" "
+            "WHERE user_name = ? "
+            "ORDER BY priority_class DESC, priority DESC"
+        )
+        rows = yield self._execute(None, sql, user_name)
+
+        dicts = []
+        for r in rows:
+            d = {}
+            for i, f in enumerate(PushRuleTable.fields):
+                d[f] = r[i]
+            dicts.append(d)
+
+        defer.returnValue(dicts)
+
+    @defer.inlineCallbacks
+    def add_push_rule(self, before, after, **kwargs):
+        vals = copy.copy(kwargs)
+        if 'conditions' in vals:
+            vals['conditions'] = json.dumps(vals['conditions'])
+        if 'actions' in vals:
+            vals['actions'] = json.dumps(vals['actions'])
+        # we could check the rest of the keys are valid column names
+        # but sqlite will do that anyway so I think it's just pointless.
+        if 'id' in vals:
+            del vals['id']
+
+        if before or after:
+            ret = yield self.runInteraction(
+                "_add_push_rule_relative_txn",
+                self._add_push_rule_relative_txn,
+                before=before,
+                after=after,
+                **vals
+            )
+            defer.returnValue(ret)
+        else:
+            ret = yield self.runInteraction(
+                "_add_push_rule_highest_priority_txn",
+                self._add_push_rule_highest_priority_txn,
+                **vals
+            )
+            defer.returnValue(ret)
+
+    def _add_push_rule_relative_txn(self, txn, user_name, **kwargs):
+        after = None
+        relative_to_rule = None
+        if 'after' in kwargs and kwargs['after']:
+            after = kwargs['after']
+            relative_to_rule = after
+        if 'before' in kwargs and kwargs['before']:
+            relative_to_rule = kwargs['before']
+
+        # get the priority of the rule we're inserting after/before
+        sql = (
+            "SELECT priority_class, priority FROM ? "
+            "WHERE user_name = ? and rule_id = ?" % (PushRuleTable.table_name,)
+        )
+        txn.execute(sql, (user_name, relative_to_rule))
+        res = txn.fetchall()
+        if not res:
+            raise RuleNotFoundException("before/after rule not found: %s" % (relative_to_rule))
+        priority_class, base_rule_priority = res[0]
+
+        if 'priority_class' in kwargs and kwargs['priority_class'] != priority_class:
+            raise InconsistentRuleException(
+                "Given priority class does not match class of relative rule"
+            )
+
+        new_rule = copy.copy(kwargs)
+        if 'before' in new_rule:
+            del new_rule['before']
+        if 'after' in new_rule:
+            del new_rule['after']
+        new_rule['priority_class'] = priority_class
+        new_rule['user_name'] = user_name
+
+        # check if the priority before/after is free
+        new_rule_priority = base_rule_priority
+        if after:
+            new_rule_priority -= 1
+        else:
+            new_rule_priority += 1
+
+        new_rule['priority'] = new_rule_priority
+
+        sql = (
+            "SELECT COUNT(*) FROM "+PushRuleTable.table_name+
+            " WHERE user_name = ? AND priority_class = ? AND priority = ?"
+        )
+        txn.execute(sql, (user_name, priority_class, new_rule_priority))
+        res = txn.fetchall()
+        num_conflicting = res[0][0]
+
+        # if there are conflicting rules, bump everything
+        if num_conflicting:
+            sql = "UPDATE "+PushRuleTable.table_name+" SET priority = priority "
+            if after:
+                sql += "-1"
+            else:
+                sql += "+1"
+            sql += " WHERE user_name = ? AND priority_class = ? AND priority "
+            if after:
+                sql += "<= ?"
+            else:
+                sql += ">= ?"
+
+            txn.execute(sql, (user_name, priority_class, new_rule_priority))
+
+        # now insert the new rule
+        sql = "INSERT OR REPLACE INTO "+PushRuleTable.table_name+" ("
+        sql += ",".join(new_rule.keys())+") VALUES ("
+        sql += ", ".join(["?" for _ in new_rule.keys()])+")"
+
+        txn.execute(sql, new_rule.values())
+
+    def _add_push_rule_highest_priority_txn(self, txn, user_name, priority_class, **kwargs):
+        # find the highest priority rule in that class
+        sql = (
+            "SELECT COUNT(*), MAX(priority) FROM "+PushRuleTable.table_name+
+            " WHERE user_name = ? and priority_class = ?"
+        )
+        txn.execute(sql, (user_name, priority_class))
+        res = txn.fetchall()
+        (how_many, highest_prio) = res[0]
+
+        new_prio = 0
+        if how_many > 0:
+            new_prio = highest_prio + 1
+
+        # and insert the new rule
+        new_rule = copy.copy(kwargs)
+        if 'id' in new_rule:
+            del new_rule['id']
+        new_rule['user_name'] = user_name
+        new_rule['priority_class'] = priority_class
+        new_rule['priority'] = new_prio
+
+        sql = "INSERT OR REPLACE INTO "+PushRuleTable.table_name+" ("
+        sql += ",".join(new_rule.keys())+") VALUES ("
+        sql += ", ".join(["?" for _ in new_rule.keys()])+")"
+
+        txn.execute(sql, new_rule.values())
+
+    @defer.inlineCallbacks
+    def delete_push_rule(self, user_name, rule_id):
+        yield self._simple_delete_one(
+            PushRuleTable.table_name,
+            {
+                'user_name': user_name,
+                'rule_id': rule_id
+            }
+        )
+
+
+class RuleNotFoundException(Exception):
+    pass
+
+
+class InconsistentRuleException(Exception):
+    pass
+
+
+class PushRuleTable(Table):
+    table_name = "push_rules"
+
+    fields = [
+        "id",
+        "user_name",
+        "rule_id",
+        "priority_class",
+        "priority",
+        "conditions",
+        "actions",
+    ]
+
+    EntryType = collections.namedtuple("PushRuleEntry", fields)
\ No newline at end of file
diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py
new file mode 100644
index 0000000000..113cdc8a8e
--- /dev/null
+++ b/synapse/storage/pusher.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+
+from ._base import SQLBaseStore, Table
+from twisted.internet import defer
+
+from synapse.api.errors import StoreError
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class PusherStore(SQLBaseStore):
+    @defer.inlineCallbacks
+    def get_pushers_by_app_id_and_pushkey(self, app_id_and_pushkey):
+        sql = (
+            "SELECT id, user_name, kind, instance_handle, app_id,"
+            "app_display_name, device_display_name, pushkey, ts, data, "
+            "last_token, last_success, failing_since "
+            "FROM pushers "
+            "WHERE app_id = ? AND pushkey = ?"
+        )
+
+        rows = yield self._execute(
+            None, sql, app_id_and_pushkey[0], app_id_and_pushkey[1]
+        )
+
+        ret = [
+            {
+                "id": r[0],
+                "user_name": r[1],
+                "kind": r[2],
+                "instance_handle": r[3],
+                "app_id": r[4],
+                "app_display_name": r[5],
+                "device_display_name": r[6],
+                "pushkey": r[7],
+                "pushkey_ts": r[8],
+                "data": r[9],
+                "last_token": r[10],
+                "last_success": r[11],
+                "failing_since": r[12]
+            }
+            for r in rows
+        ]
+
+        defer.returnValue(ret[0])
+
+    @defer.inlineCallbacks
+    def get_all_pushers(self):
+        sql = (
+            "SELECT id, user_name, kind, instance_handle, app_id,"
+            "app_display_name, device_display_name, pushkey, ts, data, "
+            "last_token, last_success, failing_since "
+            "FROM pushers"
+        )
+
+        rows = yield self._execute(None, sql)
+
+        ret = [
+            {
+                "id": r[0],
+                "user_name": r[1],
+                "kind": r[2],
+                "instance_handle": r[3],
+                "app_id": r[4],
+                "app_display_name": r[5],
+                "device_display_name": r[6],
+                "pushkey": r[7],
+                "pushkey_ts": r[8],
+                "data": r[9],
+                "last_token": r[10],
+                "last_success": r[11],
+                "failing_since": r[12]
+            }
+            for r in rows
+        ]
+
+        defer.returnValue(ret)
+
+    @defer.inlineCallbacks
+    def add_pusher(self, user_name, instance_handle, kind, app_id,
+                   app_display_name, device_display_name,
+                   pushkey, pushkey_ts, lang, data):
+        try:
+            yield self._simple_upsert(
+                PushersTable.table_name,
+                dict(
+                    app_id=app_id,
+                    pushkey=pushkey,
+                ),
+                dict(
+                    user_name=user_name,
+                    kind=kind,
+                    instance_handle=instance_handle,
+                    app_display_name=app_display_name,
+                    device_display_name=device_display_name,
+                    ts=pushkey_ts,
+                    lang=lang,
+                    data=data
+                ))
+        except Exception as e:
+            logger.error("create_pusher with failed: %s", e)
+            raise StoreError(500, "Problem creating pusher.")
+
+    @defer.inlineCallbacks
+    def delete_pusher_by_app_id_pushkey(self, app_id, pushkey):
+        yield self._simple_delete_one(
+            PushersTable.table_name,
+            dict(app_id=app_id, pushkey=pushkey)
+        )
+
+    @defer.inlineCallbacks
+    def update_pusher_last_token(self, user_name, pushkey, last_token):
+        yield self._simple_update_one(
+            PushersTable.table_name,
+            {'user_name': user_name, 'pushkey': pushkey},
+            {'last_token': last_token}
+        )
+
+    @defer.inlineCallbacks
+    def update_pusher_last_token_and_success(self, user_name, pushkey,
+                                             last_token, last_success):
+        yield self._simple_update_one(
+            PushersTable.table_name,
+            {'user_name': user_name, 'pushkey': pushkey},
+            {'last_token': last_token, 'last_success': last_success}
+        )
+
+    @defer.inlineCallbacks
+    def update_pusher_failing_since(self, user_name, pushkey, failing_since):
+        yield self._simple_update_one(
+            PushersTable.table_name,
+            {'user_name': user_name, 'pushkey': pushkey},
+            {'failing_since': failing_since}
+        )
+
+
+class PushersTable(Table):
+    table_name = "pushers"
+
+    fields = [
+        "id",
+        "user_name",
+        "kind",
+        "instance_handle",
+        "app_id",
+        "app_display_name",
+        "device_display_name",
+        "pushkey",
+        "pushkey_ts",
+        "data",
+        "last_token",
+        "last_success",
+        "failing_since"
+    ]
+
+    EntryType = collections.namedtuple("PusherEntry", fields)
\ No newline at end of file
diff --git a/synapse/storage/room.py b/synapse/storage/room.py
index 978b2c4a48..6542f8e4f8 100644
--- a/synapse/storage/room.py
+++ b/synapse/storage/room.py
@@ -58,13 +58,6 @@ class RoomStore(SQLBaseStore):
             logger.error("store_room with room_id=%s failed: %s", room_id, e)
             raise StoreError(500, "Problem creating room.")
 
-    def store_room_config(self, room_id, visibility):
-        return self._simple_update_one(
-            table=RoomsTable.table_name,
-            keyvalues={"room_id": room_id},
-            updatevalues={"is_public": visibility}
-        )
-
     def get_room(self, room_id):
         """Retrieve a room.
 
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index e59e65529b..c69dd995ce 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -20,6 +20,7 @@ from collections import namedtuple
 from ._base import SQLBaseStore
 
 from synapse.api.constants import Membership
+from synapse.types import UserID
 
 import logging
 
@@ -39,7 +40,7 @@ class RoomMemberStore(SQLBaseStore):
         """
         try:
             target_user_id = event.state_key
-            domain = self.hs.parse_userid(target_user_id).domain
+            domain = UserID.from_string(target_user_id).domain
         except:
             logger.exception(
                 "Failed to parse target_user_id=%s", target_user_id
@@ -84,7 +85,7 @@ class RoomMemberStore(SQLBaseStore):
             for e in member_events:
                 try:
                     joined_domains.add(
-                        self.hs.parse_userid(e.state_key).domain
+                        UserID.from_string(e.state_key).domain
                     )
                 except:
                     # FIXME: How do we deal with invalid user ids in the db?
diff --git a/synapse/storage/schema/delta/v12.sql b/synapse/storage/schema/delta/v12.sql
new file mode 100644
index 0000000000..8c4dfd5c1b
--- /dev/null
+++ b/synapse/storage/schema/delta/v12.sql
@@ -0,0 +1,46 @@
+/* Copyright 2014 OpenMarket Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-- Push notification endpoints that users have configured
+CREATE TABLE IF NOT EXISTS pushers (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_name TEXT NOT NULL,
+  instance_handle varchar(32) NOT NULL,
+  kind varchar(8) NOT NULL,
+  app_id varchar(64) NOT NULL,
+  app_display_name varchar(64) NOT NULL,
+  device_display_name varchar(128) NOT NULL,
+  pushkey blob NOT NULL,
+  ts BIGINT NOT NULL,
+  lang varchar(8),
+  data blob,
+  last_token TEXT,
+  last_success BIGINT,
+  failing_since BIGINT,
+  FOREIGN KEY(user_name) REFERENCES users(name),
+  UNIQUE (app_id, pushkey)
+);
+
+CREATE TABLE IF NOT EXISTS push_rules (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_name TEXT NOT NULL,
+  rule_id TEXT NOT NULL,
+  priority_class TINYINT NOT NULL,
+  priority INTEGER NOT NULL DEFAULT 0,
+  conditions TEXT NOT NULL,
+  actions TEXT NOT NULL,
+  UNIQUE(user_name, rule_id)
+);
+
+CREATE INDEX IF NOT EXISTS push_rules_user_name on push_rules (user_name);
diff --git a/synapse/storage/schema/pusher.sql b/synapse/storage/schema/pusher.sql
new file mode 100644
index 0000000000..8c4dfd5c1b
--- /dev/null
+++ b/synapse/storage/schema/pusher.sql
@@ -0,0 +1,46 @@
+/* Copyright 2014 OpenMarket Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-- Push notification endpoints that users have configured
+CREATE TABLE IF NOT EXISTS pushers (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_name TEXT NOT NULL,
+  instance_handle varchar(32) NOT NULL,
+  kind varchar(8) NOT NULL,
+  app_id varchar(64) NOT NULL,
+  app_display_name varchar(64) NOT NULL,
+  device_display_name varchar(128) NOT NULL,
+  pushkey blob NOT NULL,
+  ts BIGINT NOT NULL,
+  lang varchar(8),
+  data blob,
+  last_token TEXT,
+  last_success BIGINT,
+  failing_since BIGINT,
+  FOREIGN KEY(user_name) REFERENCES users(name),
+  UNIQUE (app_id, pushkey)
+);
+
+CREATE TABLE IF NOT EXISTS push_rules (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_name TEXT NOT NULL,
+  rule_id TEXT NOT NULL,
+  priority_class TINYINT NOT NULL,
+  priority INTEGER NOT NULL DEFAULT 0,
+  conditions TEXT NOT NULL,
+  actions TEXT NOT NULL,
+  UNIQUE(user_name, rule_id)
+);
+
+CREATE INDEX IF NOT EXISTS push_rules_user_name on push_rules (user_name);
diff --git a/synapse/storage/state.py b/synapse/storage/state.py
index 5327517704..71db16d0e5 100644
--- a/synapse/storage/state.py
+++ b/synapse/storage/state.py
@@ -78,12 +78,6 @@ class StateStore(SQLBaseStore):
             f,
         )
 
-    def store_state_groups(self, event):
-        return self.runInteraction(
-            "store_state_groups",
-            self._store_state_groups_txn, event
-        )
-
     def _store_state_groups_txn(self, txn, event, context):
         if context.current_state is None:
             return
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index bedc3c6c52..8ac2adab05 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -39,6 +39,8 @@ from ._base import SQLBaseStore
 from synapse.api.errors import SynapseError
 from synapse.util.logutils import log_function
 
+from collections import namedtuple
+
 import logging
 
 
@@ -52,91 +54,79 @@ _STREAM_TOKEN = "stream"
 _TOPOLOGICAL_TOKEN = "topological"
 
 
-def _parse_stream_token(string):
-    try:
-        if string[0] != 's':
-            raise
-        return int(string[1:])
-    except:
-        raise SynapseError(400, "Invalid token")
-
-
-def _parse_topological_token(string):
-    try:
-        if string[0] != 't':
-            raise
-        parts = string[1:].split('-', 1)
-        return (int(parts[0]), int(parts[1]))
-    except:
-        raise SynapseError(400, "Invalid token")
-
-
-def is_stream_token(string):
-    try:
-        _parse_stream_token(string)
-        return True
-    except:
-        return False
-
-
-def is_topological_token(string):
-    try:
-        _parse_topological_token(string)
-        return True
-    except:
-        return False
-
-
-def _get_token_bound(token, comparison):
-    try:
-        s = _parse_stream_token(token)
-        return "%s %s %d" % ("stream_ordering", comparison, s)
-    except:
-        pass
-
-    try:
-        top, stream = _parse_topological_token(token)
-        return "%s %s %d AND %s %s %d" % (
-            "topological_ordering", comparison, top,
-            "stream_ordering", comparison, stream,
-        )
-    except:
-        pass
-
-    raise SynapseError(400, "Invalid token")
-
-
-class StreamStore(SQLBaseStore):
-    @log_function
-    def get_room_events(self, user_id, from_key, to_key, room_id, limit=0,
-                        direction='f', with_feedback=False):
-        # We deal with events request in two different ways depending on if
-        # this looks like an /events request or a pagination request.
-        is_events = (
-            direction == 'f'
-            and user_id
-            and is_stream_token(from_key)
-            and to_key and is_stream_token(to_key)
-        )
+class _StreamToken(namedtuple("_StreamToken", "topological stream")):
+    """Tokens are positions between events. The token "s1" comes after event 1.
+
+            s0    s1
+            |     |
+        [0] V [1] V [2]
+
+    Tokens can either be a point in the live event stream or a cursor going
+    through historic events.
+
+    When traversing the live event stream events are ordered by when they
+    arrived at the homeserver.
+
+    When traversing historic events the events are ordered by their depth in
+    the event graph "topological_ordering" and then by when they arrived at the
+    homeserver "stream_ordering".
+
+    Live tokens start with an "s" followed by the "stream_ordering" id of the
+    event it comes after. Historic tokens start with a "t" followed by the
+    "topological_ordering" id of the event it comes after, follewed by "-",
+    followed by the "stream_ordering" id of the event it comes after.
+    """
+    __slots__ = []
+
+    @classmethod
+    def parse(cls, string):
+        try:
+            if string[0] == 's':
+                return cls(None, int(string[1:]))
+            if string[0] == 't':
+                parts = string[1:].split('-', 1)
+                return cls(int(parts[1]), int(parts[0]))
+        except:
+            pass
+        raise SynapseError(400, "Invalid token %r" % (string,))
+
+    @classmethod
+    def parse_stream_token(cls, string):
+        try:
+            if string[0] == 's':
+                return cls(None, int(string[1:]))
+        except:
+            pass
+        raise SynapseError(400, "Invalid token %r" % (string,))
+
+    def __str__(self):
+        if self.topological is not None:
+            return "t%d-%d" % (self.topological, self.stream)
+        else:
+            return "s%d" % (self.stream,)
 
-        if is_events:
-            return self.get_room_events_stream(
-                user_id=user_id,
-                from_key=from_key,
-                to_key=to_key,
-                room_id=room_id,
-                limit=limit,
-                with_feedback=with_feedback,
+    def lower_bound(self):
+        if self.topological is None:
+            return "(%d < %s)" % (self.stream, "stream_ordering")
+        else:
+            return "(%d < %s OR (%d == %s AND %d < %s))" % (
+                self.topological, "topological_ordering",
+                self.topological, "topological_ordering",
+                self.stream, "stream_ordering",
             )
+
+    def upper_bound(self):
+        if self.topological is None:
+            return "(%d >= %s)" % (self.stream, "stream_ordering")
         else:
-            return self.paginate_room_events(
-                from_key=from_key,
-                to_key=to_key,
-                room_id=room_id,
-                limit=limit,
-                with_feedback=with_feedback,
+            return "(%d > %s OR (%d == %s AND %d >= %s))" % (
+                self.topological, "topological_ordering",
+                self.topological, "topological_ordering",
+                self.stream, "stream_ordering",
             )
 
+
+class StreamStore(SQLBaseStore):
     @log_function
     def get_room_events_stream(self, user_id, from_key, to_key, room_id,
                                limit=0, with_feedback=False):
@@ -162,8 +152,8 @@ class StreamStore(SQLBaseStore):
             limit = MAX_STREAM_SIZE
 
         # From and to keys should be integers from ordering.
-        from_id = _parse_stream_token(from_key)
-        to_id = _parse_stream_token(to_key)
+        from_id = _StreamToken.parse_stream_token(from_key)
+        to_id = _StreamToken.parse_stream_token(to_key)
 
         if from_key == to_key:
             return defer.succeed(([], to_key))
@@ -181,7 +171,7 @@ class StreamStore(SQLBaseStore):
         }
 
         def f(txn):
-            txn.execute(sql, (user_id, user_id, from_id, to_id,))
+            txn.execute(sql, (user_id, user_id, from_id.stream, to_id.stream,))
 
             rows = self.cursor_to_dict(txn)
 
@@ -211,17 +201,21 @@ class StreamStore(SQLBaseStore):
         # Tokens really represent positions between elements, but we use
         # the convention of pointing to the event before the gap. Hence
         # we have a bit of asymmetry when it comes to equalities.
-        from_comp = '<=' if direction == 'b' else '>'
-        to_comp = '>' if direction == 'b' else '<='
-        order = "DESC" if direction == 'b' else "ASC"
-
         args = [room_id]
-
-        bounds = _get_token_bound(from_key, from_comp)
-        if to_key:
-            bounds = "%s AND %s" % (
-                bounds, _get_token_bound(to_key, to_comp)
-            )
+        if direction == 'b':
+            order = "DESC"
+            bounds = _StreamToken.parse(from_key).upper_bound()
+            if to_key:
+                bounds = "%s AND %s" % (
+                    bounds, _StreamToken.parse(to_key).lower_bound()
+                )
+        else:
+            order = "ASC"
+            bounds = _StreamToken.parse(from_key).lower_bound()
+            if to_key:
+                bounds = "%s AND %s" % (
+                    bounds, _StreamToken.parse(to_key).upper_bound()
+                )
 
         if int(limit) > 0:
             args.append(int(limit))
@@ -249,9 +243,13 @@ class StreamStore(SQLBaseStore):
                 topo = rows[-1]["topological_ordering"]
                 toke = rows[-1]["stream_ordering"]
                 if direction == 'b':
-                    topo -= 1
+                    # Tokens are positions between events.
+                    # This token points *after* the last event in the chunk.
+                    # We need it to point to the event before it in the chunk
+                    # when we are going backwards so we subtract one from the
+                    # stream part.
                     toke -= 1
-                next_token = "t%s-%s" % (topo, toke)
+                next_token = str(_StreamToken(topo, toke))
             else:
                 # TODO (erikj): We should work out what to do here instead.
                 next_token = to_key if to_key else from_key
@@ -284,9 +282,14 @@ class StreamStore(SQLBaseStore):
             rows.reverse()  # As we selected with reverse ordering
 
             if rows:
+                # Tokens are positions between events.
+                # This token points *after* the last event in the chunk.
+                # We need it to point to the event before it in the chunk
+                # since we are going backwards so we subtract one from the
+                # stream part.
                 topo = rows[0]["topological_ordering"]
-                toke = rows[0]["stream_ordering"]
-                start_token = "t%s-%s" % (topo, toke)
+                toke = rows[0]["stream_ordering"] - 1
+                start_token = str(_StreamToken(topo, toke))
 
                 token = (start_token, end_token)
             else:
diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py
index 8e164e4be0..22119de46a 100644
--- a/tests/handlers/test_directory.py
+++ b/tests/handlers/test_directory.py
@@ -21,6 +21,7 @@ from mock import Mock
 
 from synapse.server import HomeServer
 from synapse.handlers.directory import DirectoryHandler
+from synapse.types import RoomAlias
 
 from tests.utils import SQLiteMemoryDbPool, MockKey
 
@@ -65,9 +66,9 @@ class DirectoryTestCase(unittest.TestCase):
 
         self.store = hs.get_datastore()
 
-        self.my_room = hs.parse_roomalias("#my-room:test")
-        self.your_room = hs.parse_roomalias("#your-room:test")
-        self.remote_room = hs.parse_roomalias("#another:remote")
+        self.my_room = RoomAlias.from_string("#my-room:test")
+        self.your_room = RoomAlias.from_string("#your-room:test")
+        self.remote_room = RoomAlias.from_string("#another:remote")
 
     @defer.inlineCallbacks
     def test_get_local_association(self):
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index b85a89052a..5621a8afaf 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -17,7 +17,7 @@
 from tests import unittest
 from twisted.internet import defer, reactor
 
-from mock import Mock, call, ANY, NonCallableMock, patch
+from mock import Mock, call, ANY, NonCallableMock
 import json
 
 from tests.utils import (
@@ -31,6 +31,7 @@ from synapse.api.errors import SynapseError
 from synapse.handlers.presence import PresenceHandler, UserPresenceCache
 from synapse.streams.config import SourcePaginationConfig
 from synapse.storage.transactions import DestinationsTable
+from synapse.types import UserID
 
 OFFLINE = PresenceState.OFFLINE
 UNAVAILABLE = PresenceState.UNAVAILABLE
@@ -59,68 +60,216 @@ class JustPresenceHandlers(object):
     def __init__(self, hs):
         self.presence_handler = PresenceHandler(hs)
 
-class PresenceStateTestCase(unittest.TestCase):
-    """ Tests presence management. """
 
+class PresenceTestCase(unittest.TestCase):
     @defer.inlineCallbacks
     def setUp(self):
-        db_pool = SQLiteMemoryDbPool()
-        yield db_pool.prepare()
+        self.clock = MockClock()
 
         self.mock_config = NonCallableMock()
         self.mock_config.signing_key = [MockKey()]
 
+        self.mock_federation_resource = MockHttpResource()
+
+        self.mock_http_client = Mock(spec=[])
+        self.mock_http_client.put_json = DeferredMockCallable()
+
+        db_pool = None
+        hs_kwargs = {}
+
+        if hasattr(self, "make_datastore_mock"):
+            hs_kwargs["datastore"] = self.make_datastore_mock()
+        else:
+            db_pool = SQLiteMemoryDbPool()
+            yield db_pool.prepare()
+
         hs = HomeServer("test",
-            clock=MockClock(),
+            clock=self.clock,
             db_pool=db_pool,
             handlers=None,
-            resource_for_federation=Mock(),
-            http_client=None,
+            resource_for_federation=self.mock_federation_resource,
+            http_client=self.mock_http_client,
             config=self.mock_config,
             keyring=Mock(),
+            **hs_kwargs
         )
         hs.handlers = JustPresenceHandlers(hs)
 
-        self.store = hs.get_datastore()
+        self.datastore = hs.get_datastore()
 
-        # Mock the RoomMemberHandler
-        room_member_handler = Mock(spec=[])
-        hs.handlers.room_member_handler = room_member_handler
+        self.setUp_roommemberhandler_mocks(hs.handlers)
 
-        # Some local users to test with
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
-        self.u_clementine = hs.parse_userid("@clementine:test")
+        self.handler = hs.get_handlers().presence_handler
+        self.event_source = hs.get_event_sources().sources["presence"]
 
-        yield self.store.create_presence(self.u_apple.localpart)
-        yield self.store.set_presence_state(
-            self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
-        )
+        self.distributor = hs.get_distributor()
+        self.distributor.declare("user_joined_room")
 
-        self.handler = hs.get_handlers().presence_handler
+        yield self.setUp_users(hs)
 
+    def setUp_roommemberhandler_mocks(self, handlers):
+        self.room_id = "a-room"
         self.room_members = []
 
+        room_member_handler = handlers.room_member_handler = Mock(spec=[
+            "get_rooms_for_user",
+            "get_room_members",
+            "fetch_room_distributions_into",
+        ])
+        self.room_member_handler = room_member_handler
+
         def get_rooms_for_user(user):
             if user in self.room_members:
-                return defer.succeed(["a-room"])
+                return defer.succeed([self.room_id])
             else:
                 return defer.succeed([])
         room_member_handler.get_rooms_for_user = get_rooms_for_user
 
         def get_room_members(room_id):
-            if room_id == "a-room":
+            if room_id == self.room_id:
                 return defer.succeed(self.room_members)
             else:
                 return defer.succeed([])
         room_member_handler.get_room_members = get_room_members
 
+        @defer.inlineCallbacks
+        def fetch_room_distributions_into(room_id, localusers=None,
+                remotedomains=None, ignore_user=None):
+
+            members = yield get_room_members(room_id)
+            for member in members:
+                if ignore_user is not None and member == ignore_user:
+                    continue
+
+                if member.is_mine:
+                    if localusers is not None:
+                        localusers.add(member)
+                else:
+                    if remotedomains is not None:
+                        remotedomains.add(member.domain)
+        room_member_handler.fetch_room_distributions_into = (
+                fetch_room_distributions_into)
+
+        self.setUp_datastore_room_mocks(self.datastore)
+
+    def setUp_datastore_room_mocks(self, datastore):
+        def get_room_hosts(room_id):
+            if room_id == self.room_id:
+                hosts = set([u.domain for u in self.room_members])
+                return defer.succeed(hosts)
+            else:
+                return defer.succeed([])
+        datastore.get_joined_hosts_for_room = get_room_hosts
+
         def user_rooms_intersect(userlist):
             room_member_ids = map(lambda u: u.to_string(), self.room_members)
 
             shared = all(map(lambda i: i in room_member_ids, userlist))
             return defer.succeed(shared)
-        self.store.user_rooms_intersect = user_rooms_intersect
+        datastore.user_rooms_intersect = user_rooms_intersect
+
+    @defer.inlineCallbacks
+    def setUp_users(self, hs):
+        # Some local users to test with
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
+        self.u_clementine = UserID.from_string("@clementine:test")
+
+        for u in self.u_apple, self.u_banana, self.u_clementine:
+            yield self.datastore.create_presence(u.localpart)
+
+        yield self.datastore.set_presence_state(
+            self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
+        )
+
+        # ID of a local user that does not exist
+        self.u_durian = UserID.from_string("@durian:test")
+
+        # A remote user
+        self.u_cabbage = UserID.from_string("@cabbage:elsewhere")
+
+
+class MockedDatastorePresenceTestCase(PresenceTestCase):
+    def make_datastore_mock(self):
+        datastore = Mock(spec=[
+            # Bits that Federation needs
+            "prep_send_transaction",
+            "delivered_txn",
+            "get_received_txn_response",
+            "set_received_txn_response",
+            "get_destination_retry_timings",
+        ])
+
+        self.setUp_datastore_federation_mocks(datastore)
+        self.setUp_datastore_presence_mocks(datastore)
+
+        return datastore
+
+    def setUp_datastore_federation_mocks(self, datastore):
+        datastore.get_destination_retry_timings.return_value = (
+            defer.succeed(DestinationsTable.EntryType("", 0, 0))
+        )
+
+        def get_received_txn_response(*args):
+            return defer.succeed(None)
+        datastore.get_received_txn_response = get_received_txn_response
+
+    def setUp_datastore_presence_mocks(self, datastore):
+        self.current_user_state = {
+            "apple": OFFLINE,
+            "banana": OFFLINE,
+            "clementine": OFFLINE,
+            "fig": OFFLINE,
+        }
+
+        def get_presence_state(user_localpart):
+            return defer.succeed(
+                    {"state": self.current_user_state[user_localpart],
+                     "status_msg": None,
+                     "mtime": 123456000}
+            )
+        datastore.get_presence_state = get_presence_state
+
+        def set_presence_state(user_localpart, new_state):
+            was = self.current_user_state[user_localpart]
+            self.current_user_state[user_localpart] = new_state["state"]
+            return defer.succeed({"state": was})
+        datastore.set_presence_state = set_presence_state
+
+        def get_presence_list(user_localpart, accepted):
+            if not user_localpart in self.PRESENCE_LIST:
+                return defer.succeed([])
+            return defer.succeed([
+                {"observed_user_id": u} for u in
+                self.PRESENCE_LIST[user_localpart]])
+        datastore.get_presence_list = get_presence_list
+
+        def is_presence_visible(observed_localpart, observer_userid):
+            return True
+        datastore.is_presence_visible = is_presence_visible
+
+    @defer.inlineCallbacks
+    def setUp_users(self, hs):
+        # Some local users to test with
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
+        self.u_clementine = UserID.from_string("@clementine:test")
+        self.u_durian = UserID.from_string("@durian:test")
+        self.u_elderberry = UserID.from_string("@elderberry:test")
+        self.u_fig = UserID.from_string("@fig:test")
+
+        # Remote user
+        self.u_onion = UserID.from_string("@onion:farm")
+        self.u_potato = UserID.from_string("@potato:remote")
+
+        yield
+
+
+class PresenceStateTestCase(PresenceTestCase):
+    """ Tests presence management. """
+    @defer.inlineCallbacks
+    def setUp(self):
+        yield super(PresenceStateTestCase, self).setUp()
 
         self.mock_start = Mock()
         self.mock_stop = Mock()
@@ -141,7 +290,7 @@ class PresenceStateTestCase(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_get_allowed_state(self):
-        yield self.store.allow_presence_visible(
+        yield self.datastore.allow_presence_visible(
             observed_localpart=self.u_apple.localpart,
             observer_userid=self.u_banana.to_string(),
         )
@@ -189,7 +338,7 @@ class PresenceStateTestCase(unittest.TestCase):
             {"state": UNAVAILABLE,
              "status_msg": "Away",
              "mtime": 1000000},
-            (yield self.store.get_presence_state(self.u_apple.localpart))
+            (yield self.datastore.get_presence_state(self.u_apple.localpart))
         )
 
         self.mock_start.assert_called_with(self.u_apple,
@@ -206,49 +355,11 @@ class PresenceStateTestCase(unittest.TestCase):
         self.mock_stop.assert_called_with(self.u_apple)
 
 
-class PresenceInvitesTestCase(unittest.TestCase):
+class PresenceInvitesTestCase(PresenceTestCase):
     """ Tests presence management. """
-
     @defer.inlineCallbacks
     def setUp(self):
-        self.mock_http_client = Mock(spec=[])
-        self.mock_http_client.put_json = DeferredMockCallable()
-
-        self.mock_federation_resource = MockHttpResource()
-
-        db_pool = SQLiteMemoryDbPool()
-        yield db_pool.prepare()
-
-        self.mock_config = NonCallableMock()
-        self.mock_config.signing_key = [MockKey()]
-
-        hs = HomeServer("test",
-            clock=MockClock(),
-            db_pool=db_pool,
-            handlers=None,
-            resource_for_client=Mock(),
-            resource_for_federation=self.mock_federation_resource,
-            http_client=self.mock_http_client,
-            config=self.mock_config,
-            keyring=Mock(),
-        )
-        hs.handlers = JustPresenceHandlers(hs)
-
-        self.store = hs.get_datastore()
-
-        # Some local users to test with
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
-        yield self.store.create_presence(self.u_apple.localpart)
-        yield self.store.create_presence(self.u_banana.localpart)
-
-        # ID of a local user that does not exist
-        self.u_durian = hs.parse_userid("@durian:test")
-
-        # A remote user
-        self.u_cabbage = hs.parse_userid("@cabbage:elsewhere")
-
-        self.handler = hs.get_handlers().presence_handler
+        yield super(PresenceInvitesTestCase, self).setUp()
 
         self.mock_start = Mock()
         self.mock_stop = Mock()
@@ -266,10 +377,10 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [{"observed_user_id": "@banana:test", "accepted": 1}],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
         self.assertTrue(
-            (yield self.store.is_presence_visible(
+            (yield self.datastore.is_presence_visible(
                 observed_localpart=self.u_banana.localpart,
                 observer_userid=self.u_apple.to_string(),
             ))
@@ -285,7 +396,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
     @defer.inlineCallbacks
@@ -310,7 +421,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [{"observed_user_id": "@cabbage:elsewhere", "accepted": 0}],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
         yield put_json.await_calls()
@@ -345,7 +456,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
         )
 
         self.assertTrue(
-            (yield self.store.is_presence_visible(
+            (yield self.datastore.is_presence_visible(
                 observed_localpart=self.u_apple.localpart,
                 observer_userid=self.u_cabbage.to_string(),
             ))
@@ -384,7 +495,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_accepted_remote(self):
-        yield self.store.add_presence_list_pending(
+        yield self.datastore.add_presence_list_pending(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_cabbage.to_string(),
         )
@@ -401,7 +512,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [{"observed_user_id": "@cabbage:elsewhere", "accepted": 1}],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
         self.mock_start.assert_called_with(
@@ -409,7 +520,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_denied_remote(self):
-        yield self.store.add_presence_list_pending(
+        yield self.datastore.add_presence_list_pending(
             observer_localpart=self.u_apple.localpart,
             observed_userid="@eggplant:elsewhere",
         )
@@ -426,16 +537,16 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
     @defer.inlineCallbacks
     def test_drop_local(self):
-        yield self.store.add_presence_list_pending(
+        yield self.datastore.add_presence_list_pending(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_banana.to_string(),
         )
-        yield self.store.set_presence_list_accepted(
+        yield self.datastore.set_presence_list_accepted(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_banana.to_string(),
         )
@@ -447,7 +558,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
         self.mock_stop.assert_called_with(
@@ -455,11 +566,11 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_drop_remote(self):
-        yield self.store.add_presence_list_pending(
+        yield self.datastore.add_presence_list_pending(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_cabbage.to_string(),
         )
-        yield self.store.set_presence_list_accepted(
+        yield self.datastore.set_presence_list_accepted(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_cabbage.to_string(),
         )
@@ -471,16 +582,16 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals(
             [],
-            (yield self.store.get_presence_list(self.u_apple.localpart))
+            (yield self.datastore.get_presence_list(self.u_apple.localpart))
         )
 
     @defer.inlineCallbacks
     def test_get_presence_list(self):
-        yield self.store.add_presence_list_pending(
+        yield self.datastore.add_presence_list_pending(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_banana.to_string(),
         )
-        yield self.store.set_presence_list_accepted(
+        yield self.datastore.set_presence_list_accepted(
             observer_localpart=self.u_apple.localpart,
             observed_userid=self.u_banana.to_string(),
         )
@@ -495,7 +606,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
         ], presence)
 
 
-class PresencePushTestCase(unittest.TestCase):
+class PresencePushTestCase(MockedDatastorePresenceTestCase):
     """ Tests steady-state presence status updates.
 
     They assert that presence state update messages are pushed around the place
@@ -505,138 +616,9 @@ class PresencePushTestCase(unittest.TestCase):
     presence handler; namely the _local_pushmap and _remote_recvmap.
     BE WARNED...
     """
-    def setUp(self):
-        self.clock = MockClock()
-
-        self.mock_http_client = Mock(spec=[])
-        self.mock_http_client.put_json = DeferredMockCallable()
-
-        self.mock_federation_resource = MockHttpResource()
-
-        self.mock_config = NonCallableMock()
-        self.mock_config.signing_key = [MockKey()]
-
-        hs = HomeServer("test",
-                clock=self.clock,
-                db_pool=None,
-                datastore=Mock(spec=[
-                    "set_presence_state",
-                    "get_joined_hosts_for_room",
-
-                    # Bits that Federation needs
-                    "prep_send_transaction",
-                    "delivered_txn",
-                    "get_received_txn_response",
-                    "set_received_txn_response",
-                    "get_destination_retry_timings",
-                ]),
-                handlers=None,
-                resource_for_client=Mock(),
-                resource_for_federation=self.mock_federation_resource,
-                http_client=self.mock_http_client,
-                config=self.mock_config,
-                keyring=Mock(),
-            )
-        hs.handlers = JustPresenceHandlers(hs)
-
-        self.datastore = hs.get_datastore()
-        self.datastore.get_destination_retry_timings.return_value = (
-            defer.succeed(DestinationsTable.EntryType("", 0, 0))
-        )
-
-        def get_received_txn_response(*args):
-            return defer.succeed(None)
-        self.datastore.get_received_txn_response = get_received_txn_response
-
-        self.handler = hs.get_handlers().presence_handler
-        self.event_source = hs.get_event_sources().sources["presence"]
-
-        # Mock the RoomMemberHandler
-        hs.handlers.room_member_handler = Mock(spec=[
-            "get_rooms_for_user",
-            "get_room_members",
-        ])
-        self.room_member_handler = hs.handlers.room_member_handler
-
-        self.room_members = []
-
-        def get_rooms_for_user(user):
-            if user in self.room_members:
-                return defer.succeed(["a-room"])
-            else:
-                return defer.succeed([])
-        self.room_member_handler.get_rooms_for_user = get_rooms_for_user
-
-        def get_room_members(room_id):
-            if room_id == "a-room":
-                return defer.succeed(self.room_members)
-            else:
-                return defer.succeed([])
-        self.room_member_handler.get_room_members = get_room_members
-
-        def get_room_hosts(room_id):
-            if room_id == "a-room":
-                hosts = set([u.domain for u in self.room_members])
-                return defer.succeed(hosts)
-            else:
-                return defer.succeed([])
-        self.datastore.get_joined_hosts_for_room = get_room_hosts
-
-        def user_rooms_intersect(userlist):
-            room_member_ids = map(lambda u: u.to_string(), self.room_members)
-
-            shared = all(map(lambda i: i in room_member_ids, userlist))
-            return defer.succeed(shared)
-        self.datastore.user_rooms_intersect = user_rooms_intersect
-
-        @defer.inlineCallbacks
-        def fetch_room_distributions_into(room_id, localusers=None,
-                remotedomains=None, ignore_user=None):
-
-            members = yield get_room_members(room_id)
-            for member in members:
-                if ignore_user is not None and member == ignore_user:
-                    continue
-
-                if member.is_mine:
-                    if localusers is not None:
-                        localusers.add(member)
-                else:
-                    if remotedomains is not None:
-                        remotedomains.add(member.domain)
-        self.room_member_handler.fetch_room_distributions_into = (
-                fetch_room_distributions_into)
-
-        def get_presence_list(user_localpart, accepted=None):
-            if user_localpart == "apple":
-                return defer.succeed([
-                    {"observed_user_id": "@banana:test"},
-                    {"observed_user_id": "@clementine:test"},
-                ])
-            else:
-                return defer.succeed([])
-        self.datastore.get_presence_list = get_presence_list
-
-        def is_presence_visible(observer_userid, observed_localpart):
-            if (observed_localpart == "clementine" and
-                observer_userid == "@banana:test"):
-                return False
-            return False
-        self.datastore.is_presence_visible = is_presence_visible
-
-        self.distributor = hs.get_distributor()
-        self.distributor.declare("user_joined_room")
-
-        # Some local users to test with
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
-        self.u_clementine = hs.parse_userid("@clementine:test")
-        self.u_durian = hs.parse_userid("@durian:test")
-        self.u_elderberry = hs.parse_userid("@elderberry:test")
-
-        # Remote user
-        self.u_onion = hs.parse_userid("@onion:farm")
-        self.u_potato = hs.parse_userid("@potato:remote")
+    PRESENCE_LIST = {
+            'apple': [ "@banana:test", "@clementine:test" ],
+    }
 
     @defer.inlineCallbacks
     def test_push_local(self):
@@ -911,7 +893,7 @@ class PresencePushTestCase(unittest.TestCase):
         )
 
         yield self.distributor.fire("user_joined_room", self.u_clementine,
-            "a-room"
+            self.room_id
         )
 
         self.room_members.append(self.u_clementine)
@@ -974,7 +956,7 @@ class PresencePushTestCase(unittest.TestCase):
         self.room_members = [self.u_apple, self.u_banana]
 
         yield self.distributor.fire("user_joined_room", self.u_potato,
-            "a-room"
+            self.room_id
         )
 
         yield put_json.await_calls()
@@ -1003,13 +985,13 @@ class PresencePushTestCase(unittest.TestCase):
         self.room_members.append(self.u_potato)
 
         yield self.distributor.fire("user_joined_room", self.u_clementine,
-            "a-room"
+            self.room_id
         )
 
         put_json.await_calls()
 
 
-class PresencePollingTestCase(unittest.TestCase):
+class PresencePollingTestCase(MockedDatastorePresenceTestCase):
     """ Tests presence status polling. """
 
     # For this test, we have three local users; apple is watching and is
@@ -1022,106 +1004,18 @@ class PresencePollingTestCase(unittest.TestCase):
             'fig': [ "@potato:remote" ],
     }
 
-
+    @defer.inlineCallbacks
     def setUp(self):
-        self.mock_http_client = Mock(spec=[])
-        self.mock_http_client.put_json = DeferredMockCallable()
-
-        self.mock_federation_resource = MockHttpResource()
-
-        self.mock_config = NonCallableMock()
-        self.mock_config.signing_key = [MockKey()]
-
-        hs = HomeServer("test",
-                clock=MockClock(),
-                db_pool=None,
-                datastore=Mock(spec=[
-                    # Bits that Federation needs
-                    "prep_send_transaction",
-                    "delivered_txn",
-                    "get_received_txn_response",
-                    "set_received_txn_response",
-                    "get_destination_retry_timings",
-                ]),
-                handlers=None,
-                resource_for_client=Mock(),
-                resource_for_federation=self.mock_federation_resource,
-                http_client=self.mock_http_client,
-                config=self.mock_config,
-                keyring=Mock(),
-            )
-        hs.handlers = JustPresenceHandlers(hs)
-
-        self.datastore = hs.get_datastore()
-        self.datastore.get_destination_retry_timings.return_value = (
-            defer.succeed(DestinationsTable.EntryType("", 0, 0))
-        )
-
-        def get_received_txn_response(*args):
-            return defer.succeed(None)
-        self.datastore.get_received_txn_response = get_received_txn_response
+        yield super(PresencePollingTestCase, self).setUp()
 
         self.mock_update_client = Mock()
 
         def update(*args,**kwargs):
-            # print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,)
             return defer.succeed(None)
-
         self.mock_update_client.side_effect = update
 
-        self.handler = hs.get_handlers().presence_handler
         self.handler.push_update_to_clients = self.mock_update_client
 
-        hs.handlers.room_member_handler = Mock(spec=[
-            "get_rooms_for_user",
-        ])
-        # For this test no users are ever in rooms
-        def get_rooms_for_user(user):
-            return defer.succeed([])
-        hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user
-
-        # Mocked database state
-        # Local users always start offline
-        self.current_user_state = {
-            "apple": OFFLINE,
-            "banana": OFFLINE,
-            "clementine": OFFLINE,
-            "fig": OFFLINE,
-        }
-
-        def get_presence_state(user_localpart):
-            return defer.succeed(
-                    {"state": self.current_user_state[user_localpart],
-                     "status_msg": None,
-                     "mtime": 123456000}
-            )
-        self.datastore.get_presence_state = get_presence_state
-
-        def set_presence_state(user_localpart, new_state):
-            was = self.current_user_state[user_localpart]
-            self.current_user_state[user_localpart] = new_state["state"]
-            return defer.succeed({"state": was})
-        self.datastore.set_presence_state = set_presence_state
-
-        def get_presence_list(user_localpart, accepted):
-            return defer.succeed([
-                {"observed_user_id": u} for u in
-                self.PRESENCE_LIST[user_localpart]])
-        self.datastore.get_presence_list = get_presence_list
-
-        def is_presence_visible(observed_localpart, observer_userid):
-            return True
-        self.datastore.is_presence_visible = is_presence_visible
-
-        # Local users
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
-        self.u_clementine = hs.parse_userid("@clementine:test")
-        self.u_fig = hs.parse_userid("@fig:test")
-
-        # Remote users
-        self.u_potato = hs.parse_userid("@potato:remote")
-
     @defer.inlineCallbacks
     def test_push_local(self):
         # apple goes online
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index 0584e4c8b9..3cdbb186ae 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -27,6 +27,7 @@ from synapse.server import HomeServer
 from synapse.api.constants import PresenceState
 from synapse.handlers.presence import PresenceHandler
 from synapse.handlers.profile import ProfileHandler
+from synapse.types import UserID
 
 
 OFFLINE = PresenceState.OFFLINE
@@ -136,12 +137,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
                 lambda u: defer.succeed([]))
 
         # Some local users to test with
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
-        self.u_clementine = hs.parse_userid("@clementine:test")
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
+        self.u_clementine = UserID.from_string("@clementine:test")
 
         # Remote user
-        self.u_potato = hs.parse_userid("@potato:remote")
+        self.u_potato = UserID.from_string("@potato:remote")
 
         self.mock_get_joined = (
             self.datastore.get_rooms_for_user_where_membership_is
diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py
index 25b172aa5e..7b9590c110 100644
--- a/tests/handlers/test_profile.py
+++ b/tests/handlers/test_profile.py
@@ -22,7 +22,7 @@ from mock import Mock, NonCallableMock
 from synapse.api.errors import AuthError
 from synapse.server import HomeServer
 from synapse.handlers.profile import ProfileHandler
-from synapse.api.constants import Membership
+from synapse.types import UserID
 
 from tests.utils import SQLiteMemoryDbPool, MockKey
 
@@ -71,9 +71,9 @@ class ProfileTestCase(unittest.TestCase):
 
         self.store = hs.get_datastore()
 
-        self.frank = hs.parse_userid("@1234ABCD:test")
-        self.bob   = hs.parse_userid("@4567:test")
-        self.alice = hs.parse_userid("@alice:remote")
+        self.frank = UserID.from_string("@1234ABCD:test")
+        self.bob   = UserID.from_string("@4567:test")
+        self.alice = UserID.from_string("@alice:remote")
 
         yield self.store.create_profile(self.frank.localpart)
 
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
index 0cb8aa4fbc..9a23b3812d 100644
--- a/tests/handlers/test_room.py
+++ b/tests/handlers/test_room.py
@@ -15,12 +15,13 @@
 
 
 from twisted.internet import defer
-from tests import unittest
+from .. import unittest
 
 from synapse.api.constants import EventTypes, Membership
 from synapse.handlers.room import RoomMemberHandler, RoomCreationHandler
 from synapse.handlers.profile import ProfileHandler
 from synapse.server import HomeServer
+from synapse.types import UserID
 from ..utils import MockKey
 
 from mock import Mock, NonCallableMock
@@ -164,7 +165,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
             event, context=context,
         )
         self.notifier.on_new_room_event.assert_called_once_with(
-            event, extra_users=[self.hs.parse_userid(target_user_id)]
+            event, extra_users=[UserID.from_string(target_user_id)]
         )
         self.assertFalse(self.datastore.get_room.called)
         self.assertFalse(self.datastore.store_room.called)
@@ -174,7 +175,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
     def test_simple_join(self):
         room_id = "!foo:red"
         user_id = "@bob:red"
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         join_signal_observer = Mock()
         self.distributor.observe("user_joined_room", join_signal_observer)
@@ -223,7 +224,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
         yield room_handler.change_membership(event, context)
 
         self.federation.handle_new_event.assert_called_once_with(
-            event, None, destinations=set()
+            event, destinations=set()
         )
 
         self.datastore.persist_event.assert_called_once_with(
@@ -252,7 +253,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
     def test_simple_leave(self):
         room_id = "!foo:red"
         user_id = "@bob:red"
-        user = self.hs.parse_userid(user_id)
+        user = UserID.from_string(user_id)
 
         builder = self.hs.get_event_builder_factory().new({
             "type": EventTypes.Member,
@@ -301,7 +302,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
         yield room_handler.change_membership(event, context)
 
         self.federation.handle_new_event.assert_called_once_with(
-            event, None, destinations=set(['red'])
+            event, destinations=set(['red'])
         )
 
         self.datastore.persist_event.assert_called_once_with(
diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py
index 0d4b368a43..8a7fc028d1 100644
--- a/tests/handlers/test_typing.py
+++ b/tests/handlers/test_typing.py
@@ -27,6 +27,7 @@ from synapse.server import HomeServer
 from synapse.handlers.typing import TypingNotificationHandler
 
 from synapse.storage.transactions import DestinationsTable
+from synapse.types import UserID
 
 
 def _expect_edu(destination, edu_type, content, origin="test"):
@@ -153,11 +154,11 @@ class TypingNotificationsTestCase(unittest.TestCase):
         self.auth.check_joined_room = check_joined_room
 
         # Some local users to test with
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
 
         # Remote user
-        self.u_onion = hs.parse_userid("@onion:farm")
+        self.u_onion = UserID.from_string("@onion:farm")
 
     @defer.inlineCallbacks
     def test_started_typing_local(self):
@@ -352,3 +353,29 @@ class TypingNotificationsTestCase(unittest.TestCase):
                 }},
             ]
         )
+
+        # SYN-230 - see if we can still set after timeout
+
+        yield self.handler.started_typing(
+            target_user=self.u_apple,
+            auth_user=self.u_apple,
+            room_id=self.room_id,
+            timeout=10000,
+        )
+
+        self.on_new_user_event.assert_has_calls([
+            call(rooms=[self.room_id]),
+        ])
+        self.on_new_user_event.reset_mock()
+
+        self.assertEquals(self.event_source.get_current_key(), 3)
+        self.assertEquals(
+            self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
+            [
+                {"type": "m.typing",
+                 "room_id": self.room_id,
+                 "content": {
+                     "user_ids": [self.u_apple.to_string()],
+                }},
+            ]
+        )
diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py
index 9bff9ec169..1a84d94cd9 100644
--- a/tests/rest/__init__.py
+++ b/tests/rest/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 OpenMarket Ltd
+# Copyright 2015 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,4 +12,3 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
diff --git a/tests/rest/client/__init__.py b/tests/rest/client/__init__.py
new file mode 100644
index 0000000000..1a84d94cd9
--- /dev/null
+++ b/tests/rest/client/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/rest/client/v1/__init__.py b/tests/rest/client/v1/__init__.py
new file mode 100644
index 0000000000..9bff9ec169
--- /dev/null
+++ b/tests/rest/client/v1/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/tests/rest/test_events.py b/tests/rest/client/v1/test_events.py
index d3159e2cf4..0384ffbb3d 100644
--- a/tests/rest/test_events.py
+++ b/tests/rest/client/v1/test_events.py
@@ -19,13 +19,13 @@ from tests import unittest
 # twisted imports
 from twisted.internet import defer
 
-import synapse.rest.events
-import synapse.rest.register
-import synapse.rest.room
+import synapse.rest.client.v1.events
+import synapse.rest.client.v1.register
+import synapse.rest.client.v1.room
 
 from synapse.server import HomeServer
 
-from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey
+from ....utils import MockHttpResource, SQLiteMemoryDbPool, MockKey
 from .utils import RestTestCase
 
 from mock import Mock, NonCallableMock
@@ -144,9 +144,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
         hs.get_clock().time_msec.return_value = 1000000
         hs.get_clock().time.return_value = 1000
 
-        synapse.rest.register.register_servlets(hs, self.mock_resource)
-        synapse.rest.events.register_servlets(hs, self.mock_resource)
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.register.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         # register an account
         self.user_id = "sid1"
diff --git a/tests/rest/test_presence.py b/tests/rest/client/v1/test_presence.py
index 769c7824bc..65d5cc4916 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/client/v1/test_presence.py
@@ -20,11 +20,14 @@ from twisted.internet import defer
 
 from mock import Mock
 
-from ..utils import MockHttpResource, MockKey
+from ....utils import MockHttpResource, MockKey
 
 from synapse.api.constants import PresenceState
 from synapse.handlers.presence import PresenceHandler
 from synapse.server import HomeServer
+from synapse.rest.client.v1 import presence
+from synapse.rest.client.v1 import events
+from synapse.types import UserID
 
 
 OFFLINE = PresenceState.OFFLINE
@@ -69,7 +72,7 @@ class PresenceStateTestCase(unittest.TestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(myid),
+                "user": UserID.from_string(myid),
                 "admin": False,
                 "device_id": None,
             }
@@ -86,9 +89,9 @@ class PresenceStateTestCase(unittest.TestCase):
             return defer.succeed([])
         room_member_handler.get_rooms_for_user = get_rooms_for_user
 
-        hs.register_servlets()
+        presence.register_servlets(hs, self.mock_resource)
 
-        self.u_apple = hs.parse_userid(myid)
+        self.u_apple = UserID.from_string(myid)
 
     @defer.inlineCallbacks
     def test_get_my_status(self):
@@ -159,12 +162,12 @@ class PresenceListTestCase(unittest.TestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(myid),
+                "user": UserID.from_string(myid),
                 "admin": False,
                 "device_id": None,
             }
 
-        room_member_handler = hs.handlers.room_member_handler = Mock(
+        hs.handlers.room_member_handler = Mock(
             spec=[
                 "get_rooms_for_user",
             ]
@@ -172,10 +175,10 @@ class PresenceListTestCase(unittest.TestCase):
 
         hs.get_auth().get_user_by_token = _get_user_by_token
 
-        hs.register_servlets()
+        presence.register_servlets(hs, self.mock_resource)
 
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
 
     @defer.inlineCallbacks
     def test_get_my_list(self):
@@ -279,11 +282,12 @@ class PresenceEventStreamTestCase(unittest.TestCase):
         hs.get_clock().time_msec.return_value = 1000000
 
         def _get_user_by_req(req=None):
-            return hs.parse_userid(myid)
+            return UserID.from_string(myid)
 
         hs.get_auth().get_user_by_req = _get_user_by_req
 
-        hs.register_servlets()
+        presence.register_servlets(hs, self.mock_resource)
+        events.register_servlets(hs, self.mock_resource)
 
         hs.handlers.room_member_handler = Mock(spec=[])
 
@@ -319,8 +323,8 @@ class PresenceEventStreamTestCase(unittest.TestCase):
 
         self.presence = hs.get_handlers().presence_handler
 
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
 
     @defer.inlineCallbacks
     def test_shortpoll(self):
diff --git a/tests/rest/test_profile.py b/tests/rest/client/v1/test_profile.py
index 3a0d1e700a..39cd68d829 100644
--- a/tests/rest/test_profile.py
+++ b/tests/rest/client/v1/test_profile.py
@@ -20,10 +20,13 @@ from twisted.internet import defer
 
 from mock import Mock, NonCallableMock
 
-from ..utils import MockHttpResource, MockKey
+from ....utils import MockHttpResource, MockKey
 
 from synapse.api.errors import SynapseError, AuthError
 from synapse.server import HomeServer
+from synapse.types import UserID
+
+from synapse.rest.client.v1 import profile
 
 myid = "@1234ABCD:test"
 PATH_PREFIX = "/_matrix/client/api/v1"
@@ -55,13 +58,13 @@ class ProfileTestCase(unittest.TestCase):
         )
 
         def _get_user_by_req(request=None):
-            return hs.parse_userid(myid)
+            return UserID.from_string(myid)
 
         hs.get_auth().get_user_by_req = _get_user_by_req
 
         hs.get_handlers().profile_handler = self.mock_handler
 
-        hs.register_servlets()
+        profile.register_servlets(hs, self.mock_resource)
 
     @defer.inlineCallbacks
     def test_get_my_name(self):
diff --git a/tests/rest/test_rooms.py b/tests/rest/client/v1/test_rooms.py
index 84fd730afc..76ed550b75 100644
--- a/tests/rest/test_rooms.py
+++ b/tests/rest/client/v1/test_rooms.py
@@ -18,19 +18,16 @@
 # twisted imports
 from twisted.internet import defer
 
-import synapse.rest.room
+import synapse.rest.client.v1.room
 from synapse.api.constants import Membership
 
 from synapse.server import HomeServer
+from synapse.types import UserID
 
-from tests import unittest
-
-# python imports
 import json
 import urllib
-import types
 
-from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey
+from ....utils import MockHttpResource, SQLiteMemoryDbPool, MockKey
 from .utils import RestTestCase
 
 from mock import Mock, NonCallableMock
@@ -70,7 +67,7 @@ class RoomPermissionsTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -82,7 +79,7 @@ class RoomPermissionsTestCase(RestTestCase):
 
         self.auth_user_id = self.rmcreator_id
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         self.auth = hs.get_auth()
 
@@ -294,7 +291,7 @@ class RoomPermissionsTestCase(RestTestCase):
         # set [invite/join/left] of self, set [invite/join/left] of other,
         # expect all 403s
         for usr in [self.user_id, self.rmcreator_id]:
-            yield self.join(room=room, user=usr, expect_code=403)
+            yield self.join(room=room, user=usr, expect_code=404)
             yield self.leave(room=room, user=usr, expect_code=403)
 
     @defer.inlineCallbacks
@@ -466,7 +463,7 @@ class RoomsMemberListTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -476,7 +473,7 @@ class RoomsMemberListTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
     def tearDown(self):
         pass
@@ -555,7 +552,7 @@ class RoomsCreateTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -565,7 +562,7 @@ class RoomsCreateTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
     def tearDown(self):
         pass
@@ -657,7 +654,7 @@ class RoomTopicTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -668,7 +665,7 @@ class RoomTopicTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         # create the room
         self.room_id = yield self.create_room_as(self.user_id)
@@ -773,7 +770,7 @@ class RoomMemberStateTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -783,7 +780,7 @@ class RoomMemberStateTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         self.room_id = yield self.create_room_as(self.user_id)
 
@@ -909,7 +906,7 @@ class RoomMessagesTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -919,7 +916,7 @@ class RoomMessagesTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         self.room_id = yield self.create_room_as(self.user_id)
 
@@ -1013,7 +1010,7 @@ class RoomInitialSyncTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -1023,12 +1020,12 @@ class RoomInitialSyncTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         # Since I'm getting my own presence I need to exist as far as presence
         # is concerned.
         hs.get_handlers().presence_handler.registered_user(
-            hs.parse_userid(self.user_id)
+            UserID.from_string(self.user_id)
         )
 
         # create the room
diff --git a/tests/rest/test_typing.py b/tests/rest/client/v1/test_typing.py
index c550294d59..c89b37d004 100644
--- a/tests/rest/test_typing.py
+++ b/tests/rest/client/v1/test_typing.py
@@ -18,10 +18,11 @@
 # twisted imports
 from twisted.internet import defer
 
-import synapse.rest.room
+import synapse.rest.client.v1.room
 from synapse.server import HomeServer
+from synapse.types import UserID
 
-from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey
+from ....utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey
 from .utils import RestTestCase
 
 from mock import Mock, NonCallableMock
@@ -36,6 +37,8 @@ class RoomTypingTestCase(RestTestCase):
 
     @defer.inlineCallbacks
     def setUp(self):
+        self.clock = MockClock()
+
         self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
         self.auth_user_id = self.user_id
 
@@ -47,6 +50,7 @@ class RoomTypingTestCase(RestTestCase):
 
         hs = HomeServer(
             "red",
+            clock=self.clock,
             db_pool=db_pool,
             http_client=None,
             replication_layer=Mock(),
@@ -66,7 +70,7 @@ class RoomTypingTestCase(RestTestCase):
 
         def _get_user_by_token(token=None):
             return {
-                "user": hs.parse_userid(self.auth_user_id),
+                "user": UserID.from_string(self.auth_user_id),
                 "admin": False,
                 "device_id": None,
             }
@@ -77,7 +81,31 @@ class RoomTypingTestCase(RestTestCase):
             return defer.succeed(None)
         hs.get_datastore().insert_client_ip = _insert_client_ip
 
-        synapse.rest.room.register_servlets(hs, self.mock_resource)
+        def get_room_members(room_id):
+            if room_id == self.room_id:
+                return defer.succeed([UserID.from_string(self.user_id)])
+            else:
+                return defer.succeed([])
+
+        @defer.inlineCallbacks
+        def fetch_room_distributions_into(room_id, localusers=None,
+                remotedomains=None, ignore_user=None):
+
+            members = yield get_room_members(room_id)
+            for member in members:
+                if ignore_user is not None and member == ignore_user:
+                    continue
+
+                if hs.is_mine(member):
+                    if localusers is not None:
+                        localusers.add(member)
+                else:
+                    if remotedomains is not None:
+                        remotedomains.add(member.domain)
+        hs.get_handlers().room_member_handler.fetch_room_distributions_into = (
+                fetch_room_distributions_into)
+
+        synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
 
         self.room_id = yield self.create_room_as(self.user_id)
         # Need another user to make notifications actually work
@@ -113,3 +141,25 @@ class RoomTypingTestCase(RestTestCase):
             '{"typing": false}'
         )
         self.assertEquals(200, code)
+
+    @defer.inlineCallbacks
+    def test_typing_timeout(self):
+        (code, _) = yield self.mock_resource.trigger("PUT",
+            "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+            '{"typing": true, "timeout": 30000}'
+        )
+        self.assertEquals(200, code)
+
+        self.assertEquals(self.event_source.get_current_key(), 1)
+
+        self.clock.advance_time(31);
+
+        self.assertEquals(self.event_source.get_current_key(), 2)
+
+        (code, _) = yield self.mock_resource.trigger("PUT",
+            "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+            '{"typing": true, "timeout": 30000}'
+        )
+        self.assertEquals(200, code)
+
+        self.assertEquals(self.event_source.get_current_key(), 3)
diff --git a/tests/rest/utils.py b/tests/rest/client/v1/utils.py
index 579441fb4a..579441fb4a 100644
--- a/tests/rest/utils.py
+++ b/tests/rest/client/v1/utils.py
diff --git a/tests/rest/client/v2_alpha/__init__.py b/tests/rest/client/v2_alpha/__init__.py
new file mode 100644
index 0000000000..f59745e13c
--- /dev/null
+++ b/tests/rest/client/v2_alpha/__init__.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+
+from mock import Mock
+
+from ....utils import MockHttpResource, MockKey
+
+from synapse.server import HomeServer
+from synapse.types import UserID
+
+
+PATH_PREFIX = "/_matrix/client/v2_alpha"
+
+
+class V2AlphaRestTestCase(unittest.TestCase):
+    # Consumer must define
+    #   USER_ID = <some string>
+    #   TO_REGISTER = [<list of REST servlets to register>]
+
+    def setUp(self):
+        self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+        mock_config = Mock()
+        mock_config.signing_key = [MockKey()]
+
+        hs = HomeServer("test",
+            db_pool=None,
+            datastore=Mock(spec=[
+                "insert_client_ip",
+            ]),
+            http_client=None,
+            resource_for_client=self.mock_resource,
+            resource_for_federation=self.mock_resource,
+            config=mock_config,
+        )
+
+        def _get_user_by_token(token=None):
+            return {
+                "user": UserID.from_string(self.USER_ID),
+                "admin": False,
+                "device_id": None,
+            }
+        hs.get_auth().get_user_by_token = _get_user_by_token
+
+        for r in self.TO_REGISTER:
+            r.register_servlets(hs, self.mock_resource)
diff --git a/tests/storage/TESTS_NEEDED_FOR b/tests/storage/TESTS_NEEDED_FOR
deleted file mode 100644
index 8e5d0cbdc4..0000000000
--- a/tests/storage/TESTS_NEEDED_FOR
+++ /dev/null
@@ -1,5 +0,0 @@
-synapse/storage/feedback.py
-synapse/storage/keys.py
-synapse/storage/pdu.py
-synapse/storage/stream.py
-synapse/storage/transactions.py
diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py
index e9c242cc07..bc9ebf35e2 100644
--- a/tests/storage/test_directory.py
+++ b/tests/storage/test_directory.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.storage.directory import DirectoryStore
+from synapse.types import RoomID, RoomAlias
 
 from tests.utils import SQLiteMemoryDbPool
 
@@ -37,8 +38,8 @@ class DirectoryStoreTestCase(unittest.TestCase):
 
         self.store = DirectoryStore(hs)
 
-        self.room = hs.parse_roomid("!abcde:test")
-        self.alias = hs.parse_roomalias("#my-room:test")
+        self.room = RoomID.from_string("!abcde:test")
+        self.alias = RoomAlias.from_string("#my-room:test")
 
     @defer.inlineCallbacks
     def test_room_to_alias(self):
diff --git a/tests/storage/test_presence.py b/tests/storage/test_presence.py
index 9655d3cf42..1ab193736b 100644
--- a/tests/storage/test_presence.py
+++ b/tests/storage/test_presence.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.storage.presence import PresenceStore
+from synapse.types import UserID
 
 from tests.utils import SQLiteMemoryDbPool, MockClock
 
@@ -37,8 +38,8 @@ class PresenceStoreTestCase(unittest.TestCase):
 
         self.store = PresenceStore(hs)
 
-        self.u_apple = hs.parse_userid("@apple:test")
-        self.u_banana = hs.parse_userid("@banana:test")
+        self.u_apple = UserID.from_string("@apple:test")
+        self.u_banana = UserID.from_string("@banana:test")
 
     @defer.inlineCallbacks
     def test_state(self):
diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py
index 5d36723c28..84381241bc 100644
--- a/tests/storage/test_profile.py
+++ b/tests/storage/test_profile.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.storage.profile import ProfileStore
+from synapse.types import UserID
 
 from tests.utils import SQLiteMemoryDbPool
 
@@ -36,7 +37,7 @@ class ProfileStoreTestCase(unittest.TestCase):
 
         self.store = ProfileStore(hs)
 
-        self.u_frank = hs.parse_userid("@frank:test")
+        self.u_frank = UserID.from_string("@frank:test")
 
     @defer.inlineCallbacks
     def test_displayname(self):
diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py
index 9806fbc69b..0713dfab64 100644
--- a/tests/storage/test_redaction.py
+++ b/tests/storage/test_redaction.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
 
 from tests.utils import SQLiteMemoryDbPool, MockKey
 
@@ -48,10 +49,10 @@ class RedactionTestCase(unittest.TestCase):
         self.handlers = hs.get_handlers()
         self.message_handler = self.handlers.message_handler
 
-        self.u_alice = hs.parse_userid("@alice:test")
-        self.u_bob = hs.parse_userid("@bob:test")
+        self.u_alice = UserID.from_string("@alice:test")
+        self.u_bob = UserID.from_string("@bob:test")
 
-        self.room1 = hs.parse_roomid("!abc123:test")
+        self.room1 = RoomID.from_string("!abc123:test")
 
         self.depth = 1
 
diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py
index 11761fe29a..71e5d34143 100644
--- a/tests/storage/test_room.py
+++ b/tests/storage/test_room.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.api.constants import EventTypes
+from synapse.types import UserID, RoomID, RoomAlias
 
 from tests.utils import SQLiteMemoryDbPool
 
@@ -38,9 +39,9 @@ class RoomStoreTestCase(unittest.TestCase):
         # management of the 'room_aliases' table
         self.store = hs.get_datastore()
 
-        self.room = hs.parse_roomid("!abcde:test")
-        self.alias = hs.parse_roomalias("#a-room-name:test")
-        self.u_creator = hs.parse_userid("@creator:test")
+        self.room = RoomID.from_string("!abcde:test")
+        self.alias = RoomAlias.from_string("#a-room-name:test")
+        self.u_creator = UserID.from_string("@creator:test")
 
         yield self.store.store_room(self.room.to_string(),
             room_creator_user_id=self.u_creator.to_string(),
@@ -57,17 +58,6 @@ class RoomStoreTestCase(unittest.TestCase):
         )
 
     @defer.inlineCallbacks
-    def test_store_room_config(self):
-        yield self.store.store_room_config(self.room.to_string(),
-            visibility=False
-        )
-
-        self.assertObjectHasAttributes(
-            {"is_public": False},
-            (yield self.store.get_room(self.room.to_string()))
-        )
-
-    @defer.inlineCallbacks
     def test_get_rooms(self):
         # get_rooms does an INNER JOIN on the room_aliases table :(
 
@@ -108,7 +98,7 @@ class RoomEventsStoreTestCase(unittest.TestCase):
         self.store = hs.get_datastore()
         self.event_factory = hs.get_event_factory();
 
-        self.room = hs.parse_roomid("!abcde:test")
+        self.room = RoomID.from_string("!abcde:test")
 
         yield self.store.store_room(self.room.to_string(),
             room_creator_user_id="@creator:text",
diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py
index a23a8189df..2b9048e2a9 100644
--- a/tests/storage/test_roommember.py
+++ b/tests/storage/test_roommember.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
 
 from tests.utils import SQLiteMemoryDbPool, MockKey
 
@@ -49,13 +50,13 @@ class RoomMemberStoreTestCase(unittest.TestCase):
         self.handlers = hs.get_handlers()
         self.message_handler = self.handlers.message_handler
 
-        self.u_alice = hs.parse_userid("@alice:test")
-        self.u_bob = hs.parse_userid("@bob:test")
+        self.u_alice = UserID.from_string("@alice:test")
+        self.u_bob = UserID.from_string("@bob:test")
 
         # User elsewhere on another host
-        self.u_charlie = hs.parse_userid("@charlie:elsewhere")
+        self.u_charlie = UserID.from_string("@charlie:elsewhere")
 
-        self.room = hs.parse_roomid("!abc123:test")
+        self.room = RoomID.from_string("!abc123:test")
 
     @defer.inlineCallbacks
     def inject_room_member(self, room, user, membership, replaces_state=None):
diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py
index 9247fc579e..b7f6e2aa80 100644
--- a/tests/storage/test_stream.py
+++ b/tests/storage/test_stream.py
@@ -19,6 +19,7 @@ from twisted.internet import defer
 
 from synapse.server import HomeServer
 from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
 
 from tests.utils import SQLiteMemoryDbPool, MockKey
 
@@ -48,11 +49,11 @@ class StreamStoreTestCase(unittest.TestCase):
         self.handlers = hs.get_handlers()
         self.message_handler = self.handlers.message_handler
 
-        self.u_alice = hs.parse_userid("@alice:test")
-        self.u_bob = hs.parse_userid("@bob:test")
+        self.u_alice = UserID.from_string("@alice:test")
+        self.u_bob = UserID.from_string("@bob:test")
 
-        self.room1 = hs.parse_roomid("!abc123:test")
-        self.room2 = hs.parse_roomid("!xyx987:test")
+        self.room1 = RoomID.from_string("!abc123:test")
+        self.room2 = RoomID.from_string("!xyx987:test")
 
         self.depth = 1
 
diff --git a/tests/test_types.py b/tests/test_types.py
index bfb9e6f548..b29a8415b1 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -42,12 +42,6 @@ class UserIDTestCase(unittest.TestCase):
         self.assertTrue(userA == userAagain)
         self.assertTrue(userA != userB)
 
-    def test_via_homeserver(self):
-        user = mock_homeserver.parse_userid("@3456ijkl:my.domain")
-
-        self.assertEquals("3456ijkl", user.localpart)
-        self.assertEquals("my.domain", user.domain)
-
 
 class RoomAliasTestCase(unittest.TestCase):
 
@@ -62,9 +56,3 @@ class RoomAliasTestCase(unittest.TestCase):
         room = RoomAlias("channel", "my.domain")
 
         self.assertEquals(room.to_string(), "#channel:my.domain")
-
-    def test_via_homeserver(self):
-        room = mock_homeserver.parse_roomalias("#elsewhere:my.domain")
-
-        self.assertEquals("elsewhere", room.localpart)
-        self.assertEquals("my.domain", room.domain)
diff --git a/tests/unittest.py b/tests/unittest.py
index a9c0e05541..fe26b7574f 100644
--- a/tests/unittest.py
+++ b/tests/unittest.py
@@ -69,6 +69,8 @@ class TestCase(unittest.TestCase):
                     return ret
 
             logging.getLogger().setLevel(level)
+            # Don't set SQL logging
+            logging.getLogger("synapse.storage").setLevel(old_level)
             return orig()
 
     def assertObjectHasAttributes(self, attrs, obj):
diff --git a/tests/utils.py b/tests/utils.py
index 731e03f517..97fa8d8181 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -138,7 +138,8 @@ class MockClock(object):
     now = 1000
 
     def __init__(self):
-        # list of tuples of (absolute_time, callback) in no particular order
+        # list of lists of [absolute_time, callback, expired] in no particular
+        # order
         self.timers = []
 
     def time(self):
@@ -154,11 +155,16 @@ class MockClock(object):
             LoggingContext.thread_local.current_context = current_context
             callback()
 
-        t = (self.now + delay, wrapped_callback)
+        t = [self.now + delay, wrapped_callback, False]
         self.timers.append(t)
+
         return t
 
     def cancel_call_later(self, timer):
+        if timer[2]:
+            raise Exception("Cannot cancel an expired timer")
+
+        timer[2] = True
         self.timers = [t for t in self.timers if t != timer]
 
     # For unit testing
@@ -168,11 +174,17 @@ class MockClock(object):
         timers = self.timers
         self.timers = []
 
-        for time, callback in timers:
+        for t in timers:
+            time, callback, expired = t
+
+            if expired:
+                raise Exception("Timer already expired")
+
             if self.now >= time:
+                t[2] = True
                 callback()
             else:
-                self.timers.append((time, callback))
+                self.timers.append(t)
 
 
 class SQLiteMemoryDbPool(ConnectionPool, object):