diff --git a/resources/static/dialog/controllers/addemail.js b/resources/static/dialog/controllers/addemail.js
index 8ebdd41e245c9f4090894f3b1ee544106d521b1a..e717ae7d787c0d4a2a6a5967ad7a3f28f1fb43eb 100644
--- a/resources/static/dialog/controllers/addemail.js
+++ b/resources/static/dialog/controllers/addemail.js
@@ -63,7 +63,7 @@ BrowserID.Modules.AddEmail = (function() {
   function cancelAddEmail(event) {
     cancelEvent(event);
 
-    this.close("cancel_add_email");
+    this.close("cancel_state");
   }
 
   var AddEmail = bid.Modules.PageModule.extend({
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index 970a9a71f02f589cc0f2415bfce255cb0d11f63c..39ae32547ede24a3cd0df70b7f8b93d1fdd08dc1 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -157,6 +157,8 @@ BrowserID.Modules.Authenticate = (function() {
 
   var Authenticate = bid.Modules.PageModule.extend({
     start: function(options) {
+      options = options || {};
+
       var self=this;
       self.renderDialog("authenticate", {
         sitename: user.getHostname(),
diff --git a/resources/static/dialog/controllers/checkregistration.js b/resources/static/dialog/controllers/checkregistration.js
index 437f9fc1d3e0aaade420f5fa44fdc63171832abf..d497b18eee914a63502f890ee2174707c8002a87 100644
--- a/resources/static/dialog/controllers/checkregistration.js
+++ b/resources/static/dialog/controllers/checkregistration.js
@@ -73,7 +73,10 @@ BrowserID.Modules.CheckRegistration = (function() {
 
     cancel: function() {
       var self=this;
-      self.close("cancel_" + self.verificationMessage);
+      // XXX this should change to cancelEmailValidation for email, but this 
+      // will work.
+      user.cancelUserValidation();
+      self.close("cancel_state");
     }
 
   });
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index 7c548e0ea3961edbe412710c556bdc37b410f204..9910691162ce80eaacd08b9e218976303dbf24ac 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -45,15 +45,9 @@ BrowserID.Modules.Dialog = (function() {
       dom = bid.DOM,
       offline = false,
       win = window,
-      subscriptions = [],
-      mediator = bid.Mediator,
       serviceManager = bid.module,
       runningService;
 
-  function subscribe(message, cb) {
-     subscriptions.push(mediator.subscribe(message, cb));
-  }
-
   function startService(name, options) {
     // Only one service outside of the main dialog allowed.
     if(runningService) {
@@ -63,7 +57,7 @@ BrowserID.Modules.Dialog = (function() {
     return serviceManager.start(name, options);
   }
 
-  function createCheckRegistrationController(email, verifier, message) {
+  function startRegCheckService(email, verifier, message) {
     this.confirmEmail = email;
 
     var controller = startService("check_registration", {
@@ -74,6 +68,41 @@ BrowserID.Modules.Dialog = (function() {
     controller.startCheck();
   }
 
+  function checkOnline() {
+    if ('onLine' in navigator && !navigator.onLine) {
+      this.doOffline();
+      return false;
+    }
+
+    return true;
+  }
+
+  function onWinUnload() {
+    // do this only if something else hasn't declared success
+    if (!self.success) {
+      bid.Storage.setStagedOnBehalfOf("");
+      self.doCancel();
+    }
+    window.teardownChannel();
+  }
+
+  function setupChannel() {
+    var self = this;
+
+    try {
+      win.setupChannel(self);
+    } catch (e) {
+      self.renderError("error", {
+        action: errors.relaySetup
+      });
+    }
+  }
+
+  function setOrigin(origin) {
+    user.setOrigin(origin);
+    dom.setInner("#sitename", user.getHostname());
+  }
+
   var Dialog = bid.Modules.PageModule.extend({
       init: function(options) {
         offline = false;
@@ -86,31 +115,19 @@ BrowserID.Modules.Dialog = (function() {
 
         var self=this;
 
-        self.domEvents = [];
         Dialog.sc.init.call(self, options);
 
         // keep track of where we are and what we do on success and error
         self.onsuccess = null;
         self.onerror = null;
 
-        try {
-          win.setupChannel(self);
-          self.stateMachine();
-        } catch (e) {
-          self.renderError("error", {
-            action: errors.relaySetup
-          });
-        }
-      },
-
-      destroy: function() {
-        var subscription;
-
-        while(subscription = subscriptions.pop()) {
-          mediator.unsubscribe(subscription);
-        }
+        // start this directly because it should always be running.
+        var machine = BrowserID.StateMachine.create();
+        machine.start({
+          controller: this
+        });
 
-        Dialog.sc.destroy.call(this);
+        setupChannel.call(self);
       },
 
       getVerifiedEmail: function(origin_url, onsuccess, onerror) {
@@ -119,134 +136,22 @@ BrowserID.Modules.Dialog = (function() {
 
       get: function(origin_url, params, onsuccess, onerror) {
         var self=this;
-        self.onsuccess = onsuccess;
-        self.onerror = onerror;
-
-        if (typeof(params) == 'undefined') {
-          params = {};
-        }
-
-        self.allowPersistent = !!params.allowPersistent;
-        self.requiredEmail = params.requiredEmail;
-
-        if ('onLine' in navigator && !navigator.onLine) {
-          self.doOffline();
-          return;
-        }
-
-        user.setOrigin(origin_url);
-        dom.setInner("#sitename", user.getHostname());
 
-        self.doCheckAuth();
+        if(checkOnline.call(self)) {
+          self.onsuccess = onsuccess;
+          self.onerror = onerror;
 
-        self.bind(win, "unload", function() {
-          // do this only if something else hasn't
-          // declared success
-          if (!self.success) {
-            bid.Storage.setStagedOnBehalfOf("");
-            self.doCancel();
-          }
-          window.teardownChannel();
-        });
-      },
+          params = params || {};
 
+          self.allowPersistent = !!params.allowPersistent;
+          self.requiredEmail = params.requiredEmail;
 
-      stateMachine: function() {
-        var self=this,
-            el = this.element;
+          setOrigin(origin_url);
 
-        subscribe("offline", function(msg, info) {
-          self.doOffline();
-        });
-
-        subscribe("xhrError", function(msg, info) {
-          //self.doXHRError(info);
-          // XXX how are we going to handle this?
-        });
-
-        subscribe("user_staged", function(msg, info) {
-          self.doConfirmUser(info.email);
-        });
-
-        subscribe("user_confirmed", function() {
-          self.doEmailConfirmed();
-        });
+          self.bind(win, "unload", onWinUnload);
 
-        subscribe("cancel_user_confirmed", function() {
-          user.cancelUserValidation();
-          self.returnFromStageCancel();
-        });
-
-        subscribe("authenticated", function(msg, info) {
-          //self.doEmailSelected(info.email);
-          // XXX benadida, lloyd - swap these two if you want to experiment with
-          // generating assertions directly from signin.
-          self.syncEmails();
-        });
-
-        subscribe("forgot_password", function(msg, info) {
-          self.doForgotPassword(info.email);
-        });
-
-        subscribe("cancel_forgot_password", function(msg, info) {
-          user.cancelUserValidation();
-          self.returnFromStageCancel();
-        });
-
-        subscribe("reset_password", function(msg, info) {
-          self.doConfirmUser(info.email);
-        });
-
-        subscribe("assertion_generated", function(msg, info) {
-          if (info.assertion !== null) {
-            self.doAssertionGenerated(info.assertion);
-          }
-          else {
-            self.doPickEmail();
-          }
-        });
-
-        subscribe("add_email", function(msg, info) {
-          self.doAddEmail();
-        });
-
-        subscribe("cancel_add_email", function(msg, info) {
-          self.doPickEmail();
-        });
-
-        subscribe("email_staged", function(msg, info) {
-          self.doConfirmEmail(info.email);
-        });
-
-        subscribe("email_confirmed", function() {
-          self.doEmailConfirmed();
-        });
-
-        subscribe("cancel_email_confirmed", function() {
-          user.cancelEmailValidation();
-          self.returnFromStageCancel();
-        });
-
-        subscribe("notme", function() {
-          self.doNotMe();
-        });
-
-        subscribe("auth", function(msg, info) {
-          info = info || {};
-
-          self.doAuthenticate({
-            email: info.email
-          });
-        });
-
-        subscribe("start", function() {
           self.doCheckAuth();
-        });
-
-        subscribe("cancel", function() {
-          self.doCancel();
-        });
-
+        }
       },
 
       doOffline: function() {
@@ -263,7 +168,7 @@ BrowserID.Modules.Dialog = (function() {
       },
 
       doConfirmUser: function(email) {
-        createCheckRegistrationController.call(this, email, "waitForUserValidation", "user_confirmed");
+        startRegCheckService.call(this, email, "waitForUserValidation", "user_confirmed");
       },
 
       doCancel: function() {
@@ -275,7 +180,6 @@ BrowserID.Modules.Dialog = (function() {
 
       doPickEmail: function() {
         var self=this;
-        self.returnFromStageCancel = self.doPickEmail.bind(self);
         startService("pick_email", {
           // XXX ideal is to get rid of this and have a User function
           // that takes care of getting email addresses AND the last used email
@@ -290,17 +194,10 @@ BrowserID.Modules.Dialog = (function() {
       },
 
       doAuthenticate: function(info) {
-        var self = this;
-
-        // Save this off in case the user forgets their password or goes to add 
-        // a new password but has to cancel.
-        self.returnFromStageCancel = self.doAuthenticate.bind(self, info);
         startService("authenticate", info);
       },
 
       doAuthenticateWithRequiredEmail: function(info) {
-        var self=this;
-        self.returnFromStageCancel = self.doAuthenticateWithRequiredEmail.bind(self, info);
         startService("required_email", info);
       },
 
@@ -311,13 +208,13 @@ BrowserID.Modules.Dialog = (function() {
       },
 
       doConfirmEmail: function(email) {
-        createCheckRegistrationController.call(this, email, "waitForEmailValidation", "email_confirmed");
+        startRegCheckService.call(this, email, "waitForEmailValidation", "email_confirmed");
       },
 
       doEmailConfirmed: function() {
         var self=this;
         // yay!  now we need to produce an assertion.
-        user.getAssertion(this.confirmEmail, self.doAssertionGenerated.bind(self),
+        user.getAssertion(self.confirmEmail, self.doAssertionGenerated.bind(self),
           self.getErrorDialog(errors.getAssertion));
       },
 
@@ -333,10 +230,10 @@ BrowserID.Modules.Dialog = (function() {
 
       doNotMe: function() {
         var self=this;
-        user.logoutUser(self.doAuthenticate.bind(self), self.getErrorDialog(errors.logoutUser));
+        user.logoutUser(self.publish.bind(self, "auth"), self.getErrorDialog(errors.logoutUser));
       },
 
-      syncEmails: function() {
+      doSyncThenPickEmail: function() {
         var self = this;
         user.syncEmails(self.doPickEmail.bind(self),
           self.getErrorDialog(errors.signIn));
@@ -347,15 +244,16 @@ BrowserID.Modules.Dialog = (function() {
         user.checkAuthenticationAndSync(function onSuccess() {},
           function onComplete(authenticated) {
             if (self.requiredEmail) {
+              // XXX get this out of here and into the state machine!
               self.doAuthenticateWithRequiredEmail({
                 email: self.requiredEmail,
                 authenticated: authenticated
               });
             }
             else if (authenticated) {
-              self.doPickEmail();
+              self.publish("pick_email");
             } else {
-              self.doAuthenticate();
+              self.publish("auth");
             }
           }, self.getErrorDialog(errors.checkAuthentication));
     }
diff --git a/resources/static/dialog/controllers/forgotpassword.js b/resources/static/dialog/controllers/forgotpassword.js
index 25bb28fefe21641e001df6933eea8d6e3b49a91b..730eb802b6e97140be4ce2ebc8e0ba1d327b45ea 100644
--- a/resources/static/dialog/controllers/forgotpassword.js
+++ b/resources/static/dialog/controllers/forgotpassword.js
@@ -58,7 +58,7 @@ BrowserID.Modules.ForgotPassword = (function() {
   function cancelResetPassword(event) {
     cancelEvent(event);
 
-    this.close("cancel_forgot_password");
+    this.close("cancel_state");
   }
 
   var ForgotPassword = bid.Modules.PageModule.extend({
diff --git a/resources/static/dialog/controllers/page.js b/resources/static/dialog/controllers/page.js
index 0aada5d79f91a2a1ebd0340a4a021d3c357979d7..7a09282ea9e880f85b1f007cd4da60ed4295615d 100644
--- a/resources/static/dialog/controllers/page.js
+++ b/resources/static/dialog/controllers/page.js
@@ -60,7 +60,7 @@ BrowserID.Modules.PageModule = (function() {
 
       var self=this;
 
-      this.domEvents = [];
+      self.domEvents = [];
 
       if(options.bodyTemplate) {
         self.renderDialog(options.bodyTemplate, options.bodyVars);
diff --git a/resources/static/dialog/controllers/pickemail.js b/resources/static/dialog/controllers/pickemail.js
index bec32eedb08e9ebd30e7d3523e51b795ceafbf1e..90b7ff7eba45ecd1b3a625fe4e60a9f92d29d287 100644
--- a/resources/static/dialog/controllers/pickemail.js
+++ b/resources/static/dialog/controllers/pickemail.js
@@ -108,6 +108,7 @@ BrowserID.Modules.PickEmail = (function() {
       options = options || {};
 
       self.allowPersistent = options.allow_persistent;
+      dom.addClass("body", "pickemail");
       self.renderDialog("pickemail", {
         identities: user.getStoredEmailKeypairs(),
         // XXX ideal is to get rid of self and have a User function
@@ -133,6 +134,11 @@ BrowserID.Modules.PickEmail = (function() {
       pickEmailState.call(self);
     },
 
+    stop: function() {
+      PickEmail.sc.stop.call(this);
+      dom.removeClass("body", "pickemail");
+    },
+
     signIn: signIn,
     addEmail: addEmail
   });
diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css
index ad97724288c29266c00bc2c3701af03720c0217e..e590319259888c760e432aa7f1db659311f5509d 100644
--- a/resources/static/dialog/css/popup.css
+++ b/resources/static/dialog/css/popup.css
@@ -196,16 +196,6 @@ section > .contents {
     margin-right: 40px;
 }
 
-#signOut {
-  display: none;
-  margin-right: 10px;
-}
-
-.authenticated #signOut {
-  cursor: pointer;
-  display: inline;
-}
-
 .arrow {
     width: 40px;
     height: 250px;
diff --git a/resources/static/dialog/dialog.js b/resources/static/dialog/dialog.js
index c0f655bc3d7d03f34798047ed186b7e79955c316..b0d755824ded2259fccdfc14c935365fa41d2135 100644
--- a/resources/static/dialog/dialog.js
+++ b/resources/static/dialog/dialog.js
@@ -70,6 +70,7 @@ steal.then(
                '../shared/helpers',
 
                'resources/helpers',
+               'resources/state_machine',
 
 	             'controllers/page',
                'controllers/dialog',
diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js
new file mode 100644
index 0000000000000000000000000000000000000000..6052e2821dd131e9c9297ecf0857790711641e06
--- /dev/null
+++ b/resources/static/dialog/resources/state_machine.js
@@ -0,0 +1,182 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+(function() {
+  var bid = BrowserID,
+      user = bid.User,
+      mediator = bid.Mediator,
+      subscriptions = [],
+      stateStack = [];
+
+  function subscribe(message, cb) {
+    subscriptions.push(mediator.subscribe(message, cb));
+  }
+
+  function unsubscribeAll() {
+    while(subscription = subscriptions.pop()) {
+      mediator.unsubscribe(subscription);
+    }
+  }
+
+  function pushState(funcName) {
+    var args = [].splice.call(arguments, 1),
+        controller = this.controller;
+
+    // Remember the state and the information for the state in case we have to 
+    // go back to it.
+    stateStack.push({
+      funcName: funcName,
+      args: args
+    });
+
+    controller[funcName].apply(controller, args);
+  }
+
+  // Used for when the current state is being cancelled and the user wishes to 
+  // go to the previous state.
+  function popState() {
+    // Skip the first state, it is where the user is at now.
+    stateStack.pop();
+
+    // When popping, go to the second state back.
+    var gotoState = stateStack[stateStack.length - 1];
+
+    if(gotoState) {
+      var controller = this.controller;
+      controller[gotoState.funcName].apply(controller, gotoState.args);
+    }
+  }
+
+  function startStateMachine() {
+    var self = this,
+        controller = self.controller,
+        gotoState = pushState.bind(self),
+        cancelState = popState.bind(self);
+       
+    subscribe("offline", function(msg, info) {
+      gotoState("doOffline");
+    });
+
+    subscribe("cancel_state", function(msg, info) {
+      cancelState();
+    });
+
+    subscribe("user_staged", function(msg, info) {
+      gotoState("doConfirmUser", info.email);
+    });
+
+    subscribe("user_confirmed", function() {
+      gotoState("doEmailConfirmed");
+    });
+
+    subscribe("pick_email", function() {
+      gotoState("doPickEmail");
+    });
+
+    subscribe("authenticated", function(msg, info) {
+      gotoState("doSyncThenPickEmail");
+    });
+
+    subscribe("forgot_password", function(msg, info) {
+      gotoState("doForgotPassword", info.email);
+    });
+
+    subscribe("reset_password", function(msg, info) {
+      gotoState("doConfirmUser", info.email);
+    });
+
+    subscribe("assertion_generated", function(msg, info) {
+      if (info.assertion !== null) {
+        gotoState("doAssertionGenerated", info.assertion);
+      }
+      else {
+        gotoState("doPickEmail");
+      }
+    });
+
+    subscribe("add_email", function(msg, info) {
+      gotoState("doAddEmail");
+    });
+
+    subscribe("email_staged", function(msg, info) {
+      gotoState("doConfirmEmail", info.email);
+    });
+
+    subscribe("email_confirmed", function() {
+      gotoState("doEmailConfirmed");
+    });
+
+    subscribe("notme", function() {
+      gotoState("doNotMe");
+    });
+
+    subscribe("auth", function(msg, info) {
+      info = info || {};
+
+      gotoState("doAuthenticate", {
+        email: info.email
+      });
+    });
+
+    subscribe("start", function() {
+      gotoState("doCheckAuth");
+    });
+
+    subscribe("cancel", function() {
+      gotoState("doCancel");
+    });
+  }
+
+  var StateMachine = BrowserID.Class({
+    init: function() { 
+      // empty
+    },
+
+    start: function(options) {
+      options = options || {};
+      this.controller = options.controller;
+      startStateMachine.call(this);
+    }, 
+
+    stop: function() {
+      unsubscribeAll();
+    }
+  });
+
+
+  bid.StateMachine = StateMachine;
+}());
+
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index bcb723ee31c74ec662a96ee5e57fbe755778833b..d44f8fd0cbcf2ec6c2657ba3a2e01e2b040f238e 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -42,7 +42,7 @@ BrowserID.Network = (function() {
       server_time,
       domain_key_creation_time,
       auth_status,
-      hub = window.OpenAjax && OpenAjax.hub;
+      mediator = BrowserID.Mediator;
 
   function deferResponse(cb) {
     if (cb) {
@@ -65,7 +65,7 @@ BrowserID.Network = (function() {
       network.errorThrown = errorThrown;
 
       if (cb) cb(info);
-      hub && hub.publish("xhrError", info);
+      mediator && mediator.publish("xhrError", info);
     };
   }
 
@@ -147,7 +147,7 @@ BrowserID.Network = (function() {
 
   // Not really part of the Network API, but related to networking
   $(document).bind("offline", function() {
-    hub.publish("offline");
+    mediator.publish("offline");
   });
 
   var Network = {
diff --git a/resources/static/test/qunit/controllers/addemail_unit_test.js b/resources/static/test/qunit/controllers/addemail_unit_test.js
index 38ea061f3a515d5b863b2d13ba2a315349a7060b..c0d0ece9621fdfb455654f3485c12697884298bf 100644
--- a/resources/static/test/qunit/controllers/addemail_unit_test.js
+++ b/resources/static/test/qunit/controllers/addemail_unit_test.js
@@ -85,7 +85,8 @@ steal.then(function() {
   });
 
   function createController(options) {
-    controller = modules.AddEmail.create(options);
+    controller = modules.AddEmail.create();
+    controller.start(options);
   }
 
   test("addemail controller renders correctly", function() {
@@ -151,7 +152,7 @@ steal.then(function() {
   test("cancelAddEmail", function() {
     createController();
 
-    register("cancel_add_email", function(msg, info) {
+    register("cancel_state", function(msg, info) {
       ok(true, "cancelling the add email");
       start();
     });
diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
index 0090a10f423a8f3812002c32793df9aa6909d50e..99c974ae537fca1224b3766a9a9c7d19819db3ee 100644
--- a/resources/static/test/qunit/controllers/authenticate_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -70,7 +70,9 @@ steal.then(function() {
   }
 
   function createController(options) {
-    controller = bid.Modules.Authenticate.create(options);
+    options = options || {};
+    controller = bid.Modules.Authenticate.create();
+    controller.start(options);
   }
 
   module("controllers/authenticate_controller", {
diff --git a/resources/static/test/qunit/controllers/checkregistration_unit_test.js b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
index 199d63703f1d4c05296088239dbcf2b6065b5a8a..61d3ef9870294d65651117b98d4a4c243b9487c4 100644
--- a/resources/static/test/qunit/controllers/checkregistration_unit_test.js
+++ b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
@@ -38,7 +38,6 @@ steal.then(function() {
   "use strict";
 
   var controller,
-      el,
       bid = BrowserID,
       xhr = bid.Mocks.xhr,
       network = bid.Network,
@@ -57,8 +56,8 @@ steal.then(function() {
   }
 
   function createController(verifier, message) {
-    el = $("body");
-    controller = bid.Modules.CheckRegistration.create({
+    controller = bid.Modules.CheckRegistration.create();
+    controller.start({
       email: "registered@testuser.com",
       verifier: verifier,
       verificationMessage: message
@@ -128,10 +127,10 @@ steal.then(function() {
     stop();
   });
 
-  test("cancel raises cancel_user_verified", function() {
+  test("cancel raises cancel_state", function() {
     createController("waitForUserValidation", "user_verified");
-    subscribe("cancel_user_verified", function() {
-      ok(true, "on cancel, cancel_user_verified is triggered");
+    subscribe("cancel_state", function() {
+      ok(true, "on cancel, cancel_state is triggered");
       start();
     });
     controller.startCheck();
diff --git a/resources/static/test/qunit/controllers/forgotpassword_unit_test.js b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
index 66228f6ab87e0d648cfc3f2a1cffbba429dd3780..0db3a320ba741bcecb0e997fc63adaaa64f4b300 100644
--- a/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
+++ b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
@@ -65,7 +65,8 @@ steal.then(function() {
   }
 
   function createController(options) {
-    controller = bid.Modules.ForgotPassword.create(options);
+    controller = bid.Modules.ForgotPassword.create();
+    controller.start(options);
   }
 
   module("controllers/forgotpassword_controller", {
@@ -109,14 +110,13 @@ steal.then(function() {
   });
 
   test("cancelResetPassword raises 'cancel_forgot_password'", function() {
-    register("cancel_forgot_password", function(msg, info) {
-      ok(true, "cancel_forgot_password triggered");
+    register("cancel_state", function(msg, info) {
+      ok(true, "cancel_state triggered");
       start();
     });
 
     controller.cancelResetPassword();
     stop();
-
   });
 });
 
diff --git a/resources/static/test/qunit/controllers/page_unit_test.js b/resources/static/test/qunit/controllers/page_unit_test.js
index 99295d5b3336e7c3e7677f8d700a3293af59f142..cdb15de1fe07effd1183b9aba56101e856c65cdf 100644
--- a/resources/static/test/qunit/controllers/page_unit_test.js
+++ b/resources/static/test/qunit/controllers/page_unit_test.js
@@ -54,6 +54,7 @@ steal.then(function() {
 
   function createController(options) {
     controller = bid.Modules.PageModule.create(options);
+    controller.start();
   }
 
   module("/controllers/page_controller", {
@@ -150,13 +151,7 @@ steal.then(function() {
   });
 
   test("renderError allows us to open expanded error info", function() {
-    createController({
-      waitTemplate: waitTemplate,
-      waitVars: {
-        title: "Test title",
-        message: "Test message"
-      }
-    });
+    createController();
 
     controller.renderError("error", {
       action: {
@@ -169,15 +164,17 @@ steal.then(function() {
 
     $("#moreInfo").hide();
 
-    $("#openMoreInfo").click();
+    var evt = $.Event("click");
+    $("#openMoreInfo").trigger( evt );
 
+    /*
     setTimeout(function() {
       equal($("#showMoreInfo").is(":visible"), false, "button is not visible after clicking expanded info");
       equal($("#moreInfo").is(":visible"), true, "expanded error info is visible after clicking expanded info");
       start();
     }, 500);
-
     stop();
+*/
   });
 
   test("getErrorDialog gets a function that can be used to render an error message", function() {
diff --git a/resources/static/test/qunit/controllers/pickemail_unit_test.js b/resources/static/test/qunit/controllers/pickemail_unit_test.js
index f9e2611b2f12a0cbc9c9b7d3336b4f2d69a800e9..2975e8633b33ecbef4c0a1b6cd6b1dd2af599ae9 100644
--- a/resources/static/test/qunit/controllers/pickemail_unit_test.js
+++ b/resources/static/test/qunit/controllers/pickemail_unit_test.js
@@ -93,7 +93,8 @@ steal.then(function() {
 
 
   function createController(allowPersistent) {
-    controller = bid.Modules.PickEmail.create({
+    controller = bid.Modules.PickEmail.create();
+    controller.start({
       allow_persistent: allowPersistent || false
     });
   }
diff --git a/resources/static/test/qunit/controllers/required_email_unit_test.js b/resources/static/test/qunit/controllers/required_email_unit_test.js
index 6ff0ee75c2249cc83036c314963292e25edaca8b..c7cc5fb63804274766cdeb62638864b69abfd083 100644
--- a/resources/static/test/qunit/controllers/required_email_unit_test.js
+++ b/resources/static/test/qunit/controllers/required_email_unit_test.js
@@ -87,7 +87,8 @@ steal.then(function() {
   });
 
   function createController(options) {
-    controller = bid.Modules.RequiredEmail.create(options);
+    controller = bid.Modules.RequiredEmail.create();
+    controller.start(options);
   }
 
   function testSignIn(email, cb) {
diff --git a/resources/static/test/qunit/qunit.js b/resources/static/test/qunit/qunit.js
index 91859529ac38578ab78945fd9c2164289b0a50fd..a80f36ee7432f8945c7fa3c019260a56d7c4a7ee 100644
--- a/resources/static/test/qunit/qunit.js
+++ b/resources/static/test/qunit/qunit.js
@@ -1,17 +1,19 @@
-steal.plugins(
-     "jquery",
-      "jquery/controller",
-      "jquery/controller/subscribe",
-      "funcunit/qunit")
+steal.plugins("funcunit/qunit")
   .then(
+      "/lib/jquery-1.6.2.min",
       "/lib/underscore-min",
       "/lib/ejs",
       "/lib/vepbundle",
       "/shared/browserid",
       "/lib/dom-jquery",
+      "/lib/vepbundle",
+      "/lib/openajax",
+      "/lib/module",
       "mocks/mocks",
       "mocks/xhr",
       "/shared/renderer",
+      "/shared/class",
+      "/shared/mediator",
       "/shared/screens",
       "/shared/browser-support",
       "/shared/error-messages",
@@ -25,27 +27,32 @@ steal.plugins(
 
       "/dialog/resources/channel",
       "/dialog/resources/helpers",
+      "/dialog/resources/state_machine",
+      "/dialog/resources/channel",
 
-      "/dialog/controllers/page_controller",
-      "/dialog/controllers/required_email_controller",
-      "/dialog/controllers/pickemail_controller",
-      "/dialog/controllers/addemail_controller",
-      "/dialog/controllers/authenticate_controller",
-      "/dialog/controllers/forgotpassword_controller",
+      "/dialog/controllers/page",
+      "/dialog/controllers/pickemail",
+      "/dialog/controllers/addemail",
+      "/dialog/controllers/dialog",
+      "/dialog/controllers/checkregistration",
+      "/dialog/controllers/authenticate",
+      "/dialog/controllers/forgotpassword",
+      "/dialog/controllers/required_email",
 
       "/pages/page_helpers",
 
-      "pages/browserid_unit_test",
-      "pages/page_helpers_unit_test",
-
       "include_unit_test",
       "relay/relay_unit_test",
+
+      "pages/browserid_unit_test",
+      "pages/page_helpers_unit_test",
       "pages/add_email_address_test",
       "pages/verify_email_address_test",
       "pages/forgot_unit_test",
       "pages/signin_unit_test",
       "pages/signup_unit_test",
       "pages/manage_account_unit_test",
+
       "shared/helpers_unit_test",
       "shared/renderer_unit_test",
       "shared/screens_unit_test",
@@ -56,17 +63,20 @@ steal.plugins(
       "shared/storage_unit_test",
       "shared/network_unit_test",
       "shared/user_unit_test",
-      "resources/channel_unit_test",
+
       "resources/helpers_unit_test",
+      "resources/state_machine_unit_test",
+      "resources/channel_unit_test",
 
-      "controllers/page_controller_unit_test",
-      "controllers/pickemail_controller_unit_test",
-      "controllers/addemail_controller_unit_test",
-      "controllers/dialog_controller_unit_test",
-      "controllers/checkregistration_controller_unit_test",
-      "controllers/authenticate_controller_unit_test",
-      "controllers/forgotpassword_controller_unit_test",
-      "controllers/required_email_controller_unit_test"
+      "controllers/page_unit_test",
+      "controllers/pickemail_unit_test",
+      "controllers/addemail_unit_test",
+      "controllers/checkregistration_unit_test",
+      "controllers/authenticate_unit_test",
+      "controllers/forgotpassword_unit_test",
+      "controllers/required_email_unit_test",
+      // must go last or all other tests will fail.
+      "controllers/dialog_unit_test"
       
       );
 
diff --git a/resources/static/test/qunit/resources/state_machine_unit_test.js b/resources/static/test/qunit/resources/state_machine_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f4275aae2ce6f40cab39cd3d07b46ce7a66bdc6
--- /dev/null
+++ b/resources/static/test/qunit/resources/state_machine_unit_test.js
@@ -0,0 +1,244 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.then(function() {
+  "use strict";
+  
+  var bid = BrowserID,
+      mediator = bid.Mediator,
+      machine,
+      controllerMock;
+
+  var ControllerMock = function() {}
+  ControllerMock.prototype = {
+    doOffline: function() {
+      this.offline = true;
+    },
+
+    doConfirmUser: function(email) {
+      this.email = email;
+    },
+
+    doEmailConfirmed: function() {
+      this.emailConfirmed = true;
+    },
+
+    doSyncThenPickEmail: function() {
+      // XXX rename syncEmails to something else, or have pickEmail do this?
+      this.emailsSynced = true;
+    },
+
+    doPickEmail: function() {
+      this.pickingEmail = true;
+    },
+
+    doForgotPassword: function(email) {
+      this.email = email;
+    },
+
+    doAssertionGenerated: function(assertion) {
+      // XXX what a horrible horrible name for a function
+      this.assertion = assertion;
+    },
+
+    doAddEmail: function() {
+      this.requestAddEmail = true;
+    },
+
+    doConfirmEmail: function(email) {
+      this.email = email;
+    },
+
+    doNotMe: function() {
+      this.notMe = true;
+    },
+
+    doAuthenticate: function(info) {
+      // XXX Get rid of info and pass email directly
+      this.email = info.email;
+    },
+
+    doCheckAuth: function() {
+      this.checkingAuth = true;
+    },
+
+    doCancel: function() {
+      this.cancelled = true;
+    }
+
+  };
+
+  function createMachine() {
+    machine = bid.StateMachine.create();
+    controllerMock = new ControllerMock();
+    machine.start({controller: controllerMock});
+  }
+
+  module("resources/state_machine", {
+    setup: function() {
+      createMachine();
+    },
+
+    teardown: function() {
+      machine.stop();
+    }
+  });
+
+
+  test("can create and start the machine", function() {
+    ok(machine, "Machine has been created");
+  });
+
+  test("offline does offline", function() {
+    mediator.publish("offline");
+
+    equal(controllerMock.offline, true, "controller is offline");
+  });
+  
+  test("user_staged", function() {
+    // XXX rename user_staged to confirm_user or something to that effect.
+    mediator.publish("user_staged", {
+      email: "testuser@testuser.com"
+    });
+
+    equal(controllerMock.email, "testuser@testuser.com", "waiting for email confirmation for testuser@testuser.com");
+  });
+
+  test("user_confirmed", function() {
+    mediator.publish("user_confirmed");
+
+    ok(controllerMock.emailConfirmed, "user was confirmed");
+  });
+
+  test("authenticated", function() {
+    mediator.publish("authenticated");
+
+    ok(controllerMock.emailsSynced, "emails have been synced");
+  });
+
+  test("forgot_password", function() {
+    mediator.publish("forgot_password", {
+      email: "testuser@testuser.com"
+    });
+    equal(controllerMock.email, "testuser@testuser.com", "forgot password with the correct email");
+  });
+
+  test("reset_password", function() {
+    // XXX how is this different from forgot_password?
+    mediator.publish("reset_password", {
+      email: "testuser@testuser.com"
+    });
+    equal(controllerMock.email, "testuser@testuser.com", "reset password with the correct email");
+  });
+
+  test("assertion_generated with null assertion", function() {
+    mediator.publish("assertion_generated", {
+      assertion: null
+    });
+
+    equal(controllerMock.pickingEmail, true, "now picking email because of null assertion");
+  });
+
+  test("assertion_generated with assertion", function() {
+    mediator.publish("assertion_generated", {
+      assertion: "assertion"
+    });
+
+    equal(controllerMock.assertion, "assertion", "assertion generated with good assertion");
+  });
+
+  test("add_email", function() {
+    // XXX rename add_email to request_add_email
+    mediator.publish("add_email");
+
+    ok(controllerMock.requestAddEmail, "user wants to add an email");
+  });
+
+  test("email_confirmed", function() {
+    mediator.publish("email_confirmed");
+
+    ok(controllerMock.emailConfirmed, "user has confirmed the email");
+  });
+
+  test("cancel_state", function() {
+    mediator.publish("pick_email");
+    mediator.publish("add_email");
+
+    controllerMock.pickingEmail = false;
+    mediator.publish("cancel_state");
+
+    ok(controllerMock.pickingEmail, "user is picking an email");
+  });
+
+  test("cancel_state", function() {
+    mediator.publish("add_email");
+    mediator.publish("email_staged", {
+      email: "testuser@testuser.com" 
+    });
+
+    controllerMock.requestAddEmail = false;
+    mediator.publish("cancel_state");
+
+    ok(controllerMock.requestAddEmail, "Back to trying to add an email after cancelling stage");
+  });
+
+  test("notme", function() {
+    mediator.publish("notme");
+
+    ok(controllerMock.notMe, "notMe has been called");
+  });
+
+  test("auth", function() {
+    mediator.publish("auth", {
+      email: "testuser@testuser.com" 
+    });
+
+    equal(controllerMock.email, "testuser@testuser.com", "authenticate with testuser@testuser.com");
+  });
+
+  test("start", function() {
+    mediator.publish("start");
+
+    equal(controllerMock.checkingAuth, true, "checking auth on start");
+  });
+
+  test("cancel", function() {
+    mediator.publish("cancel");
+
+    equal(controllerMock.cancelled, true, "cancelled everything");
+  });
+
+});
diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
index 525ce1c011f59d41dc76f346440b0807e81bea23..3d01e9d5b3e172c956b69261c8778f7a2276c6f3 100644
--- a/resources/static/test/qunit/shared/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -38,7 +38,9 @@ steal.then(function() {
   "use strict";
 
   var testName,
-  xhr = BrowserID.Mocks.xhr;
+      bid = BrowserID,
+      mediator = bid.Mediator,
+      xhr = bid.Mocks.xhr;
 
   function wrappedAsyncTest(name, test) {
     asyncTest(name, function() {
@@ -69,10 +71,10 @@ steal.then(function() {
       equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
       equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
       wrappedStart();
-      OpenAjax.hub.unsubscribe(handle);
+      mediator.unsubscribe(handle);
     };
 
-    handle = OpenAjax.hub.subscribe("xhrError", subscriber);
+    handle = mediator.subscribe("xhrError", subscriber);
 
     if (cb) {
       cb.apply(null, args);
@@ -680,13 +682,16 @@ steal.then(function() {
     stop();
   });
 
+  /*
   wrappedAsyncTest("body offline message triggers offline message", function() {
-    OpenAjax.hub.subscribe("offline", function() {
+    mediator.subscribe("offline", function() {
       ok(true, "offline event caught and application notified");
       start();
     });
 
-    $("body").trigger("offline");
+    var evt = $.Event("offline");
+    $("body").trigger(evt);
     stop();
   });
+  */
 });