summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore11
-rw-r--r--MANIFEST.in14
-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)0
-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/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
-rwxr-xr-xsynapse/app/homeserver.py15
-rw-r--r--synapse/config/database.py5
-rw-r--r--synapse/events/builder.py6
-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.py4
-rw-r--r--synapse/handlers/events.py2
-rw-r--r--synapse/handlers/federation.py10
-rw-r--r--synapse/handlers/typing.py10
-rw-r--r--synapse/http/matrixfederationclient.py1
-rw-r--r--synapse/notifier.py4
-rw-r--r--synapse/rest/__init__.py39
-rw-r--r--synapse/rest/client/__init__.py14
-rw-r--r--synapse/rest/client/v1/__init__.py49
-rw-r--r--synapse/rest/client/v1/admin.py (renamed from synapse/rest/admin.py)0
-rw-r--r--synapse/rest/client/v1/base.py (renamed from synapse/rest/base.py)2
-rw-r--r--synapse/rest/client/v1/directory.py (renamed from synapse/rest/directory.py)0
-rw-r--r--synapse/rest/client/v1/events.py (renamed from synapse/rest/events.py)2
-rw-r--r--synapse/rest/client/v1/initial_sync.py (renamed from synapse/rest/initial_sync.py)0
-rw-r--r--synapse/rest/client/v1/login.py (renamed from synapse/rest/login.py)0
-rw-r--r--synapse/rest/client/v1/presence.py (renamed from synapse/rest/presence.py)0
-rw-r--r--synapse/rest/client/v1/profile.py (renamed from synapse/rest/profile.py)0
-rw-r--r--synapse/rest/client/v1/push_rule.py (renamed from synapse/rest/push_rule.py)0
-rw-r--r--synapse/rest/client/v1/pusher.py (renamed from synapse/rest/pusher.py)0
-rw-r--r--synapse/rest/client/v1/register.py (renamed from synapse/rest/register.py)0
-rw-r--r--synapse/rest/client/v1/room.py (renamed from synapse/rest/room.py)2
-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)0
-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)0
-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)0
-rw-r--r--synapse/rest/media/v1/upload_resource.py (renamed from synapse/media/v1/upload_resource.py)0
-rw-r--r--synapse/server.py2
-rw-r--r--synapse/storage/room.py7
-rw-r--r--synapse/storage/state.py6
-rw-r--r--synapse/storage/stream.py201
-rw-r--r--tests/handlers/test_presence.py97
-rw-r--r--tests/handlers/test_room.py4
-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)2
-rw-r--r--tests/rest/client/v1/test_profile.py (renamed from tests/rest/test_profile.py)2
-rw-r--r--tests/rest/client/v1/test_rooms.py (renamed from tests/rest/test_rooms.py)18
-rw-r--r--tests/rest/client/v1/test_typing.py (renamed from tests/rest/test_typing.py)6
-rw-r--r--tests/rest/client/v1/utils.py (renamed from tests/rest/utils.py)0
-rw-r--r--tests/storage/TESTS_NEEDED_FOR5
-rw-r--r--tests/storage/test_room.py11
-rw-r--r--tests/utils.py20
91 files changed, 645 insertions, 1712 deletions
diff --git a/.gitignore b/.gitignore
index af90668c89..2ed22b1cd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,17 +26,12 @@ 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/
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/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 6b551d42e5..6b551d42e5 100644
--- a/graph/graph2.py
+++ b/contrib/graph/graph2.py
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/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/app/homeserver.py b/synapse/app/homeserver.py
index 3d85fda67b..6ebf1f0de0 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -26,8 +26,8 @@ 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 (
@@ -241,13 +241,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()
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/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/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..f33d17a31e 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -144,7 +144,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/events.py b/synapse/handlers/events.py
index 54ab27004f..75fb941008 100644
--- a/synapse/handlers/events.py
+++ b/synapse/handlers/events.py
@@ -70,10 +70,8 @@ class EventStreamHandler(BaseHandler):
                 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
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 195f7c618a..81203bf1a3 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -75,14 +75,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 +154,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,
                 )
 
@@ -281,7 +281,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
         )
diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py
index 22ce7873d0..cd9638dd04 100644
--- a/synapse/handlers/typing.py
+++ b/synapse/handlers/typing.py
@@ -120,6 +120,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
@@ -142,8 +146,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):
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/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/rest/__init__.py b/synapse/rest/__init__.py
index 8e5877cf3f..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,39 +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, pusher, push_rule
-)
-
-
-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)
-        pusher.register_servlets(hs, client_resource)
-        push_rule.register_servlets(hs, client_resource)
+# limitations under the License.
\ No newline at end of file
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..8e5877cf3f
--- /dev/null
+++ b/synapse/rest/client/v1/__init__.py
@@ -0,0 +1,49 @@
+# -*- 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
+)
+
+
+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)
+        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..0aa83514c8 100644
--- a/synapse/rest/admin.py
+++ b/synapse/rest/client/v1/admin.py
diff --git a/synapse/rest/base.py b/synapse/rest/client/v1/base.py
index c583945527..d005206b77 100644
--- a/synapse/rest/base.py
+++ b/synapse/rest/client/v1/base.py
@@ -15,7 +15,7 @@
 
 """ This module contains base REST classes for constructing REST servlets. """
 from synapse.api.urls import CLIENT_PREFIX
-from synapse.rest.transactions import HttpTransactionStore
+from .transactions import HttpTransactionStore
 import re
 
 import logging
diff --git a/synapse/rest/directory.py b/synapse/rest/client/v1/directory.py
index 7ff44fdd9e..7ff44fdd9e 100644
--- a/synapse/rest/directory.py
+++ b/synapse/rest/client/v1/directory.py
diff --git a/synapse/rest/events.py b/synapse/rest/client/v1/events.py
index bedcb2bcc6..c2515528ac 100644
--- a/synapse/rest/events.py
+++ b/synapse/rest/client/v1/events.py
@@ -18,7 +18,7 @@ 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 RestServlet, client_path_pattern
 
 import logging
 
diff --git a/synapse/rest/initial_sync.py b/synapse/rest/client/v1/initial_sync.py
index b13d56b286..b13d56b286 100644
--- a/synapse/rest/initial_sync.py
+++ b/synapse/rest/client/v1/initial_sync.py
diff --git a/synapse/rest/login.py b/synapse/rest/client/v1/login.py
index 6b8deff67b..6b8deff67b 100644
--- a/synapse/rest/login.py
+++ b/synapse/rest/client/v1/login.py
diff --git a/synapse/rest/presence.py b/synapse/rest/client/v1/presence.py
index ca4d2d21f0..ca4d2d21f0 100644
--- a/synapse/rest/presence.py
+++ b/synapse/rest/client/v1/presence.py
diff --git a/synapse/rest/profile.py b/synapse/rest/client/v1/profile.py
index dc6eb424b0..dc6eb424b0 100644
--- a/synapse/rest/profile.py
+++ b/synapse/rest/client/v1/profile.py
diff --git a/synapse/rest/push_rule.py b/synapse/rest/client/v1/push_rule.py
index b5e74479cf..b5e74479cf 100644
--- a/synapse/rest/push_rule.py
+++ b/synapse/rest/client/v1/push_rule.py
diff --git a/synapse/rest/pusher.py b/synapse/rest/client/v1/pusher.py
index 4659c9b1d9..4659c9b1d9 100644
--- a/synapse/rest/pusher.py
+++ b/synapse/rest/client/v1/pusher.py
diff --git a/synapse/rest/register.py b/synapse/rest/client/v1/register.py
index e3b26902d9..e3b26902d9 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/client/v1/register.py
diff --git a/synapse/rest/room.py b/synapse/rest/client/v1/room.py
index caafa959e6..48bba2a5f3 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -246,7 +246,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):
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..011c35e69b 100644
--- a/synapse/rest/voip.py
+++ b/synapse/rest/client/v1/voip.py
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 d6c6690577..d6c6690577 100644
--- a/synapse/media/v1/__init__.py
+++ b/synapse/rest/media/v1/__init__.py
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 28404f2b7b..28404f2b7b 100644
--- a/synapse/media/v1/thumbnailer.py
+++ b/synapse/rest/media/v1/thumbnailer.py
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 32d8a36db4..8e739716ad 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -24,7 +24,7 @@ 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.rest.client.v1 import RestServletFactory
 from synapse.state import StateHandler
 from synapse.storage import DataStore
 from synapse.types import UserID, RoomAlias, RoomID, EventID
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/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 744c821dfe..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,13 +282,14 @@ class StreamStore(SQLBaseStore):
             rows.reverse()  # As we selected with reverse ordering
 
             if rows:
-                # XXX: Always subtract 1 since the start token always goes
-                # backwards (parity with paginate_room_events). It isn't
-                # obvious that this is correct; we should clarify the algorithm
-                # used here.
-                topo = rows[0]["topological_ordering"] - 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
+                # since we are going backwards so we subtract one from the
+                # stream part.
+                topo = rows[0]["topological_ordering"]
                 toke = rows[0]["stream_ordering"] - 1
-                start_token = "t%s-%s" % (topo, toke)
+                start_token = str(_StreamToken(topo, toke))
 
                 token = (start_token, end_token)
             else:
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index b85a89052a..c309fbb054 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -59,23 +59,29 @@ 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()
+
         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(),
         )
@@ -92,24 +98,33 @@ class PresenceStateTestCase(unittest.TestCase):
         self.u_banana = hs.parse_userid("@banana:test")
         self.u_clementine = hs.parse_userid("@clementine:test")
 
-        yield self.store.create_presence(self.u_apple.localpart)
+        for u in self.u_apple, self.u_banana, self.u_clementine:
+            yield self.store.create_presence(u.localpart)
+
         yield self.store.set_presence_state(
             self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
         )
 
+        # 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
 
+        self.room_id = "a-room"
         self.room_members = []
 
         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([])
@@ -128,6 +143,10 @@ class PresenceStateTestCase(unittest.TestCase):
         self.handler.start_polling_presence = self.mock_start
         self.handler.stop_polling_presence = self.mock_stop
 
+
+class PresenceStateTestCase(PresenceTestCase):
+    """ Tests presence management. """
+
     @defer.inlineCallbacks
     def test_get_my_state(self):
         state = yield self.handler.get_state(
@@ -206,57 +225,10 @@ 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
-
-        self.mock_start = Mock()
-        self.mock_stop = Mock()
-
-        self.handler.start_polling_presence = self.mock_start
-        self.handler.stop_polling_presence = self.mock_stop
-
-    @defer.inlineCallbacks
     def test_invite_local(self):
         # TODO(paul): This test will likely break if/when real auth permissions
         # are added; for now the HS will always accept any invite
@@ -558,24 +530,25 @@ class PresencePushTestCase(unittest.TestCase):
         ])
         self.room_member_handler = hs.handlers.room_member_handler
 
+        self.room_id = "a-room"
         self.room_members = []
 
         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([])
         self.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([])
         self.room_member_handler.get_room_members = get_room_members
 
         def get_room_hosts(room_id):
-            if room_id == "a-room":
+            if room_id == self.room_id:
                 hosts = set([u.domain for u in self.room_members])
                 return defer.succeed(hosts)
             else:
@@ -911,7 +884,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 +947,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,7 +976,7 @@ 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()
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
index 0cb8aa4fbc..d3253b48b8 100644
--- a/tests/handlers/test_room.py
+++ b/tests/handlers/test_room.py
@@ -223,7 +223,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(
@@ -301,7 +301,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/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..0b6f7cfccb 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/client/v1/test_presence.py
@@ -20,7 +20,7 @@ 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
diff --git a/tests/rest/test_profile.py b/tests/rest/client/v1/test_profile.py
index 3a0d1e700a..47cfb10a6d 100644
--- a/tests/rest/test_profile.py
+++ b/tests/rest/client/v1/test_profile.py
@@ -20,7 +20,7 @@ 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
diff --git a/tests/rest/test_rooms.py b/tests/rest/client/v1/test_rooms.py
index 8e65ff9a1c..12f8040541 100644
--- a/tests/rest/test_rooms.py
+++ b/tests/rest/client/v1/test_rooms.py
@@ -18,7 +18,7 @@
 # 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
@@ -30,7 +30,7 @@ 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
@@ -82,7 +82,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()
 
@@ -476,7 +476,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
@@ -565,7 +565,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
@@ -668,7 +668,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)
@@ -783,7 +783,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)
 
@@ -919,7 +919,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)
 
@@ -1023,7 +1023,7 @@ 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.
diff --git a/tests/rest/test_typing.py b/tests/rest/client/v1/test_typing.py
index 18138af1b5..647bcebfd8 100644
--- a/tests/rest/test_typing.py
+++ b/tests/rest/client/v1/test_typing.py
@@ -18,10 +18,10 @@
 # twisted imports
 from twisted.internet import defer
 
-import synapse.rest.room
+import synapse.rest.client.v1.room
 from synapse.server import HomeServer
 
-from ..utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey
+from ....utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey
 from .utils import RestTestCase
 
 from mock import Mock, NonCallableMock
@@ -104,7 +104,7 @@ class RoomTypingTestCase(RestTestCase):
         hs.get_handlers().room_member_handler.fetch_room_distributions_into = (
                 fetch_room_distributions_into)
 
-        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)
         # Need another user to make notifications actually work
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/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_room.py b/tests/storage/test_room.py
index 11761fe29a..e7739776ec 100644
--- a/tests/storage/test_room.py
+++ b/tests/storage/test_room.py
@@ -57,17 +57,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 :(
 
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):