diff --git a/examples/trivial/index.js b/examples/trivial/index.js
index 5752e6bda7..2be9054954 100644
--- a/examples/trivial/index.js
+++ b/examples/trivial/index.js
@@ -24,7 +24,52 @@ var React = require("react");
 // maps cannot pass through two stages).
 var MatrixReactSdk = require("../../src/index");
 
-React.render(
-    ,
+// Here, we do some crude URL analysis to allow
+// deep-linking. We only support registration
+// deep-links in this example.
+function routeUrl(location) {
+    if (location.hash.indexOf('#/register') == 0) {
+        var hashparts = location.hash.split('?');
+        var params = {};
+        if (hashparts.length == 2) {
+            var pairs = hashparts[1].split('&');
+            for (var i = 0; i < pairs.length; ++i) {
+                var parts = pairs[i].split('=');
+                if (parts.length != 2) continue;
+                params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+            }
+        }
+        window.matrixChat.showScreen('register', params);
+    }
+}
+
+var loaded = false;
+
+window.onload = function() {
+    routeUrl(window.location);
+    loaded = true;
+}
+
+// This will be called whenever the SDK changes screens,
+// so a web page can update the URL bar appropriately.
+var onNewScreen = function(screen) {
+    if (!loaded) return;
+    window.location.hash = '#/'+screen;
+}
+
+// We use this to work out what URL the SDK should
+// pass through when registering to allow the user to
+// click back to the client having registered.
+// It's up to us to recognise if we're loaded with
+// this URL and tell MatrixClient to resume registration.
+var makeRegistrationUrl = function() {
+    return window.location.protocol + '//' +
+           window.location.host +
+           window.location.pathname +
+           '#/register';
+}
+
+window.matrixChat = React.render(
+    ,
     document.getElementById('matrixchat')
 );
diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js
index eb91b5442c..7a46f88dea 100644
--- a/skins/base/views/organisms/RoomView.js
+++ b/skins/base/views/organisms/RoomView.js
@@ -37,6 +37,12 @@ module.exports = React.createClass({
     mixins: [RoomViewController],
 
     render: function() {
+        if (!this.state.room) {
+            return (
+                
+            );
+        }
+
         var myUserId = MatrixClientPeg.get().credentials.userId;
         if (this.state.room.currentState.members[myUserId].membership == 'invite') {
             if (this.state.joining) {
diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js
index 7c37c0fe71..27d1a1151a 100644
--- a/skins/base/views/pages/MatrixChat.js
+++ b/skins/base/views/pages/MatrixChat.js
@@ -24,6 +24,7 @@ var RoomView = ComponentBroker.get('organisms/RoomView');
 var RightPanel = ComponentBroker.get('organisms/RightPanel');
 var Login = ComponentBroker.get('templates/Login');
 var UserSettings = ComponentBroker.get('organisms/UserSettings');
+var Register = ComponentBroker.get('templates/Register');
 
 var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
 
@@ -61,6 +62,14 @@ module.exports = React.createClass({
             return (
                 
             );
+        } else if (this.state.screen == 'register') {
+            return (
+                
+            );
         } else {
             return (
                 
diff --git a/skins/base/views/templates/Login.js b/skins/base/views/templates/Login.js
index 6cd721e20b..ceae07ec41 100644
--- a/skins/base/views/templates/Login.js
+++ b/skins/base/views/templates/Login.js
@@ -40,6 +40,7 @@ module.exports = React.createClass({
                     Please log in:
                     {this.componentForStep(this.state.step)}
                     {this.state.errorText}
+                    Create a new account
                 
             );
         }
diff --git a/skins/base/views/templates/Register.js b/skins/base/views/templates/Register.js
new file mode 100644
index 0000000000..676fa2e273
--- /dev/null
+++ b/skins/base/views/templates/Register.js
@@ -0,0 +1,55 @@
+/*
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+
+var ComponentBroker = require("../../../../src/ComponentBroker");
+
+var Loader = require("react-loader");
+
+var RegisterController = require("../../../../src/controllers/templates/Register");
+
+module.exports = React.createClass({
+    displayName: 'Register',
+    mixins: [RegisterController],
+
+    registerContent: function() {
+        if (this.state.busy) {
+            return (
+                
+            );
+        } else {
+            return (
+                
+            );
+        }
+    },
+
+    render: function() {
+        return (
+            
+            {this.registerContent()}
+            
+        );
+    }
+});
diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js
index 051d6e2681..dfbcf2e217 100644
--- a/src/ComponentBroker.js
+++ b/src/ComponentBroker.js
@@ -85,6 +85,7 @@ require('../skins/base/views/molecules/MemberTile');
 require('../skins/base/views/organisms/RoomList');
 require('../skins/base/views/organisms/RoomView');
 require('../skins/base/views/templates/Login');
+require('../skins/base/views/templates/Register');
 require('../skins/base/views/organisms/Notifier');
 require('../skins/base/views/organisms/CreateRoom');
 require('../skins/base/views/molecules/UserSelector');
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index a1c820ee1a..0b6c26496f 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -24,11 +24,13 @@ var matrixClient = null;
 var localStorage = window.localStorage;
 if (localStorage) {
     var hs_url = localStorage.getItem("mx_hs_url");
+    var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
     var access_token = localStorage.getItem("mx_access_token");
     var user_id = localStorage.getItem("mx_user_id");
     if (access_token && user_id && hs_url) {
         matrixClient = Matrix.createClient({
             baseUrl: hs_url,
+            idBaseUrl: is_url,
             accessToken: access_token,
             userId: user_id
         });
@@ -44,8 +46,11 @@ module.exports = {
         matrixClient = cli;
     },
 
-    replaceUsingUrl: function(hs_url) {
-        matrixClient = Matrix.createClient(hs_url);
+    replaceUsingUrls: function(hs_url, is_url) {
+        matrixClient = Matrix.createClient({
+            baseUrl: hs_url,
+            idBaseUrl: is_url
+        });
     }
 };
 
diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js
index 69b7596a60..cef2c3ac11 100644
--- a/src/controllers/organisms/RoomView.js
+++ b/src/controllers/organisms/RoomView.js
@@ -36,7 +36,7 @@ var tileTypes = {
 module.exports = {
     getInitialState: function() {
         return {
-            room: MatrixClientPeg.get().getRoom(this.props.roomId),
+            room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
             messageCap: INITIAL_SIZE
         }
     },
diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js
index d0afc3aeb6..9367872b2a 100644
--- a/src/controllers/pages/MatrixChat.js
+++ b/src/controllers/pages/MatrixChat.js
@@ -50,6 +50,11 @@ module.exports = {
         this.focusComposer = false;
         document.addEventListener("keydown", this.onKeyDown);
         window.addEventListener("focus", this.onFocus);
+        if (this.state.logged_in) {
+            this.notifyNewScreen('');
+        } else {
+            this.notifyNewScreen('login');
+        }
     },
 
     componentWillUnmount: function() {
@@ -69,14 +74,45 @@ module.exports = {
 
         switch (payload.action) {
             case 'logout':
-                this.setState({
+                this.replaceState({
                     logged_in: false,
                     ready: false
                 });
+                localStorage.removeItem("mx_hs_url");
+                localStorage.removeItem("mx_user_id");
+                localStorage.removeItem("mx_access_token");
                 Notifier.stop();
                 MatrixClientPeg.get().removeAllListeners();
                 MatrixClientPeg.replace(null);
                 break;
+            case 'start_registration':
+                if (this.state.logged_in) return;
+                var newState = payload.params || {};
+                newState.screen = 'register';
+                if (
+                    payload.params &&
+                    payload.params.client_secret &&
+                    payload.params.session_id &&
+                    payload.params.hs_url &&
+                    payload.params.is_url &&
+                    payload.params.sid
+                ) {
+                    newState.register_client_secret = payload.params.client_secret;
+                    newState.register_session_id = payload.params.session_id;
+                    newState.register_hs_url = payload.params.hs_url;
+                    newState.register_is_url = payload.params.is_url;
+                    newState.register_id_sid = payload.params.sid;
+                }
+                this.replaceState(newState);
+                this.notifyNewScreen('register');
+                break;
+            case 'start_login':
+                if (this.state.logged_in) return;
+                this.replaceState({
+                    screen: 'login'
+                });
+                this.notifyNewScreen('login');
+                break;
             case 'view_room':
                 this.focusComposer = true;
                 this.setState({
@@ -111,8 +147,12 @@ module.exports = {
     },
 
     onLoggedIn: function() {
-        this.setState({logged_in: true});
+        this.setState({
+            screen: undefined,
+            logged_in: true
+        });
         this.startMatrixClient();
+        this.notifyNewScreen('');
     },
 
     startMatrixClient: function() {
@@ -149,5 +189,25 @@ module.exports = {
 
     onFocus: function(ev) {
         dis.dispatch({action: 'focus_composer'});
+    },
+
+    showScreen(screen, params) {
+        if (screen == 'register') {
+            dis.dispatch({
+                action: 'start_registration',
+                params: params
+            });
+        } else if (screen == 'login') {
+            dis.dispatch({
+                action: 'start_login',
+                params: params
+            });
+        }
+    },
+
+    notifyNewScreen: function(screen) {
+        if (this.props.onNewScreen) {
+            this.props.onNewScreen(screen);
+        }
     }
 };
diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js
index f3c58cf2fb..714fb2720a 100644
--- a/src/controllers/templates/Login.js
+++ b/src/controllers/templates/Login.js
@@ -20,6 +20,7 @@ var React = require('react');
 
 var MatrixClientPeg = require("../../MatrixClientPeg");
 var Matrix = require("matrix-js-sdk");
+var dis = require("../../dispatcher");
 
 var ComponentBroker = require("../../ComponentBroker");
 
@@ -41,8 +42,14 @@ module.exports = {
 
     onHSChosen: function(ev) {
         ev.preventDefault();
-        MatrixClientPeg.replaceUsingUrl(this.refs.serverConfig.getHsUrl());
-        this.setState({hs_url: this.refs.serverConfig.getHsUrl()});
+        MatrixClientPeg.replaceUsingUrls(
+            this.refs.serverConfig.getHsUrl(),
+            this.refs.serverConfig.getIsUrl()
+        );
+        this.setState({
+            hs_url: this.refs.serverConfig.getHsUrl(),
+            is_url: this.refs.serverConfig.getIsUrl()
+        });
         this.setStep("fetch_stages");
         var cli = MatrixClientPeg.get();
         this.setState({busy: true});
@@ -71,12 +78,14 @@ module.exports = {
             // XXX: we assume this means we're logged in, but there could be a next stage
             MatrixClientPeg.replace(Matrix.createClient({
                 baseUrl: that.state.hs_url,
+                idBaseUrl: that.state.is_url,
                 userId: data.user_id,
                 accessToken: data.access_token
             }));
             var localStorage = window.localStorage;
             if (localStorage) {
                 localStorage.setItem("mx_hs_url", that.state.hs_url);
+                localStorage.setItem("mx_is_url", that.state.is_url);
                 localStorage.setItem("mx_user_id", data.user_id);
                 localStorage.setItem("mx_access_token", data.access_token);
             } else {
@@ -85,9 +94,6 @@ module.exports = {
             if (that.props.onLoggedIn) {
                 that.props.onLoggedIn();
             }
-            /*dis.dispatch({
-                'action': 'logged_in'
-            });*/
         }, function(error) {
             that.setStep("stage_m.login.password");
             that.setState({errorText: 'Login failed.'});
@@ -118,4 +124,11 @@ module.exports = {
                 );
         }
     },
+
+    showRegister: function(ev) {
+        ev.preventDefault();
+        dis.dispatch({
+            action: 'start_registration'
+        });
+    }
 };
diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js
new file mode 100644
index 0000000000..650fdd6b2d
--- /dev/null
+++ b/src/controllers/templates/Register.js
@@ -0,0 +1,346 @@
+/*
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+
+var MatrixClientPeg = require("../../MatrixClientPeg");
+var Matrix = require("matrix-js-sdk");
+var dis = require("../../dispatcher");
+
+var ComponentBroker = require("../../ComponentBroker");
+
+var ServerConfig = ComponentBroker.get("molecules/ServerConfig");
+
+module.exports = {
+    getInitialState: function() {
+        return {
+            step: 'initial',
+            busy: false,
+            currentStep: 0,
+            totalSteps: 1
+        };
+    },
+
+    componentWillMount: function() {
+        this.readNewProps();
+    },
+
+    componentWillReceiveProps: function() {
+        this.readNewProps();
+    },
+
+    readNewProps: function() {
+        if (this.props.clientSecret && this.props.hsUrl &&
+                this.props.isUrl && this.props.sessionId &&
+                this.props.idSid) {
+            this.authSessionId = this.props.sessionId;
+            MatrixClientPeg.replaceUsingUrls(
+                this.props.hsUrl,
+                this.props.isUrl
+            );
+            this.setState({
+                hs_url: this.props.hsUrl,
+                is_url: this.props.isUrl
+            });
+            this.savedParams = {client_secret: this.props.clientSecret};
+            this.setState({busy: true});
+
+            var isLocation = document.createElement('a');
+            isLocation.href = this.props.isUrl;
+
+            var auth = {
+                type: 'm.login.email.identity',
+                threepid_creds: {
+                    sid: this.props.idSid,
+                    client_secret: this.savedParams.client_secret,
+                    id_server: isLocation.host
+                }
+            };
+            this.tryRegister(auth);
+        }
+    },
+
+    componentDidUpdate: function() {
+        // Just putting a script tag into the returned jsx doesn't work, annoyingly,
+        // so we do this instead.
+        if (this.refs.recaptchaContainer) {
+            var scriptTag = document.createElement('script');
+            window.mx_on_recaptcha_loaded = this.onCaptchaLoaded;
+            scriptTag.setAttribute('src', "https://www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
+            this.refs.recaptchaContainer.getDOMNode().appendChild(scriptTag);
+        }
+    },
+
+    setStep: function(step) {
+        this.setState({ step: step, errorText: '', busy: false });
+    },
+
+    getSupportedStageTypes: function() {
+        return ['m.login.email.identity', 'm.login.recaptcha'];
+    },
+
+    chooseFlow: function(flows) {
+        // this is fairly simple right now
+        var supportedTypes = this.getSupportedStageTypes();
+
+        var emailFlow = null;
+        var otherFlow = null;
+        for (var flowI = 0; flowI < flows.length; ++flowI) {
+            var flow = flows[flowI];
+            var flowHasEmail = false;
+            var flowSupported = true;
+            for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
+                var stage = flow.stages[stageI];
+
+                if (supportedTypes.indexOf(stage) == -1) {
+                    flowSupported = false;
+                }
+
+                if (stage == 'm.login.email.identity') {
+                    flowHasEmail = true;
+                }
+            }
+            if (flowSupported) {
+                if (flowHasEmail) {
+                    emailFlow = flow;
+                } else {
+                    otherFlow = flow;
+                }
+            }
+        }
+
+        if (
+            this.savedParams.email != '' ||
+            this.completedStages.indexOf('m.login.email.identity' > -1)
+        ) {
+            return emailFlow;
+        } else {
+            return otherFlow;
+        }
+    },
+
+    firstUncompletedStageIndex: function(flow) {
+        if (this.completedStages === undefined) return 0;
+        for (var i = 0; i < flow.stages.length; ++i) {
+            if (this.completedStages.indexOf(flow.stages[i]) == -1) {
+                return i;
+            }
+        }
+    },
+
+    numCompletedStages: function(flow) {
+        if (this.completedStages === undefined) return 0;
+        var nCompleted = 0;
+        for (var i = 0; i < flow.stages.length; ++i) {
+            if (this.completedStages.indexOf(flow.stages[i]) > -1) {
+                ++nCompleted;
+            }
+        }
+        return nCompleted;
+    },
+
+    onInitialStageSubmit: function(ev) {
+        ev.preventDefault();
+        MatrixClientPeg.replaceUsingUrls(
+            this.refs.serverConfig.getHsUrl(),
+            this.refs.serverConfig.getIsUrl()
+        );
+        this.setState({
+            hs_url: this.refs.serverConfig.getHsUrl(),
+            is_url: this.refs.serverConfig.getIsUrl()
+        });
+        var cli = MatrixClientPeg.get();
+        this.setState({busy: true});
+        var self = this;
+
+        this.savedParams = {
+            email: this.refs.email.getDOMNode().value,
+            username: this.refs.username.getDOMNode().value,
+            password: this.refs.password.getDOMNode().value
+        };
+
+        this.tryRegister();
+    },
+
+    startStage: function(stageName) {
+        var self = this;
+        this.setStep('stage_'+stageName);
+        switch(stageName) {
+            case 'm.login.email.identity':
+                self.setState({
+                    busy: true
+                });
+                var cli = MatrixClientPeg.get();
+                this.savedParams.client_secret = cli.generateClientSecret();
+                this.savedParams.send_attempt = 1;
+
+                var nextLink = this.props.registrationUrl +
+                               '?client_secret=' +
+                               encodeURIComponent(this.savedParams.client_secret) +
+                               "&hs_url=" +
+                               encodeURIComponent(this.state.hs_url) +
+                               "&is_url=" +
+                               encodeURIComponent(this.state.is_url) +
+                               "&session_id=" +
+                               encodeURIComponent(this.authSessionId);
+
+                cli.requestEmailToken(
+                    this.savedParams.email,
+                    this.savedParams.client_secret,
+                    this.savedParams.send_attempt,
+                    nextLink
+                ).done(function(response) {
+                    self.setState({
+                        busy: false,
+                    });
+                    self.setStep('stage_m.login.email.identity');
+                }, function(error) {
+                    self.setState({
+                        busy: false,
+                        errorText: 'Unable to contact the given Home Server'
+                    });
+                });
+                break;
+            case 'm.login.recaptcha':
+                if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) {
+                    this.setState({
+                        errorText: "This server has not supplied enough information for Recaptcha authentication"
+                    });
+                }
+                break;
+        }
+    },
+
+    onRegistered: function(user_id, access_token) {
+        MatrixClientPeg.replace(Matrix.createClient({
+            baseUrl: this.state.hs_url,
+            idBaseUrl: this.state.is_url,
+            userId: user_id,
+            accessToken: access_token
+        }));
+        var localStorage = window.localStorage;
+        if (localStorage) {
+            localStorage.setItem("mx_hs_url", this.state.hs_url);
+            localStorage.setItem("mx_user_id", user_id);
+            localStorage.setItem("mx_access_token", access_token);
+        } else {
+            console.warn("No local storage available: can't persist session!");
+        }
+        if (this.props.onLoggedIn) {
+            this.props.onLoggedIn();
+        }
+    },
+
+    componentForStep: function(step) {
+        switch (step) {
+            case 'initial':
+                return (
+                    
+                        
+                    
+                );
+            // XXX: clearly these should be separate organisms
+            case 'stage_m.login.email.identity':
+                return (
+                    
+                        Please check your email to continue registration.
+                    
+                );
+            case 'stage_m.login.recaptcha':
+                return (
+                    
+                        This Home Server would like to make sure you're not a robot
+                        
+                    
 
+                );
+        }
+    },
+
+    onCaptchaLoaded: function() {
+        if (this.refs.recaptchaContainer) {
+            var sitekey = this.authParams['m.login.recaptcha'].public_key;
+            global.grecaptcha.render('mx_recaptcha', {
+                'sitekey': sitekey,
+                'callback': this.onCaptchaDone
+            });
+        }
+    },
+
+    onCaptchaDone: function(captcha_response) {
+        this.tryRegister({
+            type: 'm.login.recaptcha',
+            response: captcha_response
+        });
+    },
+
+    tryRegister: function(auth) {
+        var self = this;
+        MatrixClientPeg.get().register(
+            this.savedParams.username,
+            this.savedParams.password,
+            this.authSessionId,
+            auth
+        ).done(function(result) {
+            self.onRegistered(result.user_id, result.access_token);
+        }, function(error) {
+            if (error.httpStatus == 401 && error.data.flows) {
+                self.authParams = error.data.params;
+                self.authSessionId = error.data.session;
+
+                self.completedStages = error.data.completed;
+
+                var flow = self.chooseFlow(error.data.flows);
+
+                var flowStage = self.firstUncompletedStageIndex(flow);
+                var numDone = self.numCompletedStages(flow);
+
+                self.setState({
+                    busy: false,
+                    flows: flow,
+                    currentStep: 1+numDone,
+                    totalSteps: flow.stages.length+1,
+                    flowStage: flowStage
+                });
+                self.startStage(flow.stages[flowStage]);
+            } else {
+                var errorText = "Unable to contact the given Home Server";
+                if (error.httpStatus == 401) {
+                    errorText = "Authorisation failed!";
+                }
+                self.setStep("initial");
+                self.setState({
+                    busy: false,
+                    errorText: errorText
+                });
+            }
+        });
+    },
+
+    showLogin: function(ev) {
+        ev.preventDefault();
+        dis.dispatch({
+            action: 'start_login'
+        });
+    }
+};