diff --git a/browserid/compress.sh b/browserid/compress.sh
index 3a493518a47c68c4aae8037fcf3931ac12a50e2f..761edbf14317e3b72540919e39c44b002c1b54e2 100755
--- a/browserid/compress.sh
+++ b/browserid/compress.sh
@@ -37,7 +37,7 @@ cat popup.css m.css > production.css
 $JAVA -jar $YUI_LOCATION production.css -o production.min.css
 
 cd ../../relay
-cat ../dialog/resources/jschannel.js relay.js > production.js
+cat ../dialog/resources/jschannel.js ../dialog/resources/browserid.js relay.js > production.js
 $UGLIFY < production.js > production.min.js
 mv production.min.js production.js
 
diff --git a/browserid/static/dialog/resources/channel.js b/browserid/static/dialog/resources/channel.js
index 0d52976bb22f8dd74c7599d74dff39609716189f..eb235bd41bab1dd453d4de17eb167e2325956637 100644
--- a/browserid/static/dialog/resources/channel.js
+++ b/browserid/static/dialog/resources/channel.js
@@ -52,93 +52,90 @@
 
 
 (function() {
-  function getRelayID() {
-    return window.location.href.slice(window.location.href.indexOf('#') + 1);
-  }
+  var win = window,
+      nav = navigator,
+      onCompleteCallback;
 
   function getRelayName() {
-    return "browserid_relay_" + getRelayID();
+    var relayID = win.location.href.slice(win.location.href.indexOf('#') + 1);
+    return "browserid_relay_" + relayID;
   }
 
   function getRelayWindow() {
-    var frameWindow = window.opener.frames[getRelayName()];
+    var frameWindow = win.opener.frames[getRelayName()];
     return frameWindow;
   }
 
-  function registerWithRelayFrame(callback) {
-    var frameWindow = getRelayWindow();
-    if (frameWindow) {
-      frameWindow['register_dialog'](callback);
-    }
-  }
+  function setupNativeChannel(controller) {
+    nav.id.channel.registerController(controller);
+  };
 
-  function getRPRelay() {
-    var frameWindow = getRelayWindow();
-    return frameWindow && frameWindow['browserid_relay'];
-  }
+  function setupIFrameChannel(controller) {
+    // TODO - Add a check for whether the dialog was opened by another window
+    // (has window.opener) as well as whether the relay function exists.
+    // If these conditions are not met, then print an appropriate message.
 
+    function onsuccess(rv) {
+      onCompleteCallback(rv, null);
+    }
 
-  function errorOut(trans, code) {
-    function getVerboseMessage(code) {
-      var msgs = {
-        "canceled": "user canceled selection",
-        "notImplemented": "the user tried to invoke behavior that's not yet implemented",
-        "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers."
-      };
-      var msg = msgs[code];
-      if (!msg) {
-        alert("need verbose message for " + code);
-        msg = "unknown error";
-          }
-      return msg;
+    function onerror(error) {
+      onCompleteCallback(null, error);
     }
-    trans.error(code, getVerboseMessage(code));
-    window.self.close();
-  }
 
+    // The relay frame will give us the origin and a function to call when 
+    // dialog processing is complete.
+    var frameWindow = getRelayWindow();
+    if (frameWindow) {
+      frameWindow.BrowserID.Relay.registerClient(function(origin, onComplete) {
+        onCompleteCallback = onComplete;
+        controller.getVerifiedEmail(origin, onsuccess, onerror);
+      });
+    }
 
-  window.setupChannel = function(controller) {
-    if (navigator.id && navigator.id.channel)
+    win.location.hash = '';
+  };
+
+  function open(controller) {
+    if (nav.id && nav.id.channel)
       setupNativeChannel(controller);
     else
       setupIFrameChannel(controller);
   };
 
-  var setupNativeChannel = function(controller) {
-    navigator.id.channel.registerController(controller);
-  };
 
-  var setupIFrameChannel = function(controller) {
-    // TODO - Add a check for whether the dialog was opened by another window
-    // (has window.opener) as well as whether the relay function exists.
-    // If these conditions are not met, then print an appropriate message.
+  function init(options) {
+    onCompleteCallback = undefined;
 
-    // get the relay here at the time the channel is setup before any navigation has
-    // occured.  if we wait the window hash might change as a side effect to user
-    // navigation, which would cause us to not find our parent window.
-    // issue #295
-    var relay = getRPRelay();
-    
-    function onsuccess(rv) {
-      // Get the relay here so that we ensure that the calling window is still
-      // open and we aren't causing a problem.
-      if (relay) {
-        relay(rv, null);
-      }
+    if(options.navigator) {
+      nav = navigator;
     }
 
-    function onerror(error) {
-      if (relay) {
-        relay(null, error);
-      }
+    if(options.window) {
+      win = options.window;
     }
+  }
 
-    // The relay frame will give us the origin.
-    registerWithRelayFrame(function(origin) {
-      controller.getVerifiedEmail(origin, onsuccess, onerror);
-    });
 
-    window.location.hash = '';
-  };
+  if(window.BrowserID) {
+    BrowserID.Channel = {
+      /**
+       * Used to intialize the channel, mostly for unit testing to override 
+       * window and navigator.
+       * @method init
+       */
+      init: init,
+      /**
+       * Open the channel.
+       * @method open 
+       */
+      open: open
+    };
+  }
 
+  /**
+   * This is here as a legacy API for addons/etc that are depending on 
+   * window.setupChannel;
+   */
+  window.setupChannel = open;
 }());
diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js
index 909773ac04e93ca3c91bfee896ebbc7ba143f4f8..7ceaabc35ed72539a42d52a9a1bab5412d3525d3 100644
--- a/browserid/static/dialog/test/qunit/qunit.js
+++ b/browserid/static/dialog/test/qunit/qunit.js
@@ -16,7 +16,9 @@ steal("/dialog/resources/browserid.js",
          'pickemail.ejs')
   .then("browserid_unit_test")
   .then("include_unit_test")
+  .then("relay/relay_unit_test")
   .then("pages/add_email_address_test")
+  .then("resources/channel_unit_test")
   .then("resources/browser-support_unit_test")
   .then("resources/validation_unit_test")
   .then("resources/storage_unit_test")
diff --git a/browserid/static/dialog/test/qunit/relay/relay_unit_test.js b/browserid/static/dialog/test/qunit/relay/relay_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae7e6dc6109e84fd1d7e6957d0e208a14218212f
--- /dev/null
+++ b/browserid/static/dialog/test/qunit/relay/relay_unit_test.js
@@ -0,0 +1,183 @@
+/*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.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", function() {
+  "use strict";
+
+  var winMock = {},
+      relay = BrowserID.Relay;
+
+  var channelMock = {
+    build: function(options) {
+      this.options = options;
+
+      channelMock.bindMessage = channelMock.cb = channelMock.status = 
+        channelMock.errorCode = channelMock.verboseError = undefined;
+
+      return {
+        bind: function(message, cb) {
+          channelMock.bindMessage = message;
+          channelMock.cb = cb;
+        }
+      }
+    },
+
+    // Mock in the receiving of the RPC call from the RP.
+    receiveGetVerifiedEmail: function() {
+      // cb is the mock callback that is passed to Channel.bind
+      channelMock.cb({
+        origin: "Origin", 
+        delayReturn: function() {},
+        complete: function(status) {
+          channelMock.status = status;
+        },
+        error: function(code, verboseMessage) {
+          channelMock.errorCode = code;
+          channelMock.verboseError = verboseMessage;
+        }
+      });
+    }
+
+  };
+
+
+  module("relay/relay", {
+    setup: function() {
+      relay.init({
+        window: winMock,
+        channel: channelMock
+      });
+    },
+    teardown: function() {
+      relay.init({
+        window: window.parent,
+        channel: Channel 
+      });
+    }
+  });
+
+  test("Can open the relay, happy case", function() {
+    relay.open(); 
+
+    /**
+     * Check to make sure channel build is correct
+     */
+    equal(channelMock.options.window, winMock, "opening to the correct window");
+    equal(channelMock.options.origin, "*", "accept messages from anybody");
+    equal(channelMock.options.scope, "mozid", "mozid namespace");
+
+    /**
+     * Check to make sure the correct message is bound
+     */
+    equal(channelMock.bindMessage, "getVerifiedEmail", "bound to getVerifiedEmail");
+  });
+
+  test("channel.getVerifiedEmail before registerDialog", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      equal(origin, "Origin", "Origin set correctly");
+      equal(typeof completeCB, "function", "A completion callback is specified");
+
+      start();
+    });
+
+    stop();
+  });
+
+  test("registerDialog before channel.getVerifiedEmail", function() {
+    relay.open();
+
+    relay.registerClient(function(origin, completeCB) {
+      equal(origin, "Origin", "Origin set correctly");
+      equal(typeof completeCB, "function", "A completion callback is specified");
+
+      start();
+    });
+
+    channelMock.receiveGetVerifiedEmail();
+
+    stop();
+  });
+
+  test("calling the completeCB with assertion", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB("assertion", null);
+      equal(channelMock.status, "assertion", "channel gets the correct assertion");
+      start();
+    });
+
+    stop();
+  });
+
+
+  test("calling the completeCB with null assertion", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB(null, null);
+      strictEqual(channelMock.status, null, "channel gets the null assertion");
+      start();
+    });
+
+    stop();
+  });
+
+  test("calling the completeCB with error", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB(null, "canceled");
+
+      equal(channelMock.errorCode, "canceled", "error callback called with error code");
+      ok(channelMock.verboseError, "verbose error code set");
+
+      start();
+    });
+
+    stop();
+  });
+});
diff --git a/browserid/static/dialog/test/qunit/resources/channel_unit_test.js b/browserid/static/dialog/test/qunit/resources/channel_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..da72df2344699d499d668ad582b1640bd98c6c5a
--- /dev/null
+++ b/browserid/static/dialog/test/qunit/resources/channel_unit_test.js
@@ -0,0 +1,137 @@
+/*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.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", function() {
+  var channel = BrowserID.Channel;
+
+  var navMock = {
+    id: {},
+  };
+
+  // Mock in the window object as well as the frame relay
+  var winMock = {
+    location: {
+      href: "browserid.org/sign_in#1234"
+    },
+    opener: {
+      frames: {
+        browserid_relay_1234: {
+          BrowserID: {
+            Relay: {
+              registerClient: function(callback) {
+                // mock of the registerClient function in the BrowserID.Channel.
+                callback("origin", function onComplete(success, error) {
+                  winMock.success = success;
+                  winMock.error = error;
+                });
+              }
+            }
+          }
+        }
+      }
+    }
+  };
+
+  module("resources/channel", {
+    setup: function() {
+    },
+
+    teardown: function() {
+      channel.init({
+        window: window,
+        navigator: navigator
+      });
+    }
+  });
+
+  test("window.setupChannel exists for legacy uses", function() {
+    ok(typeof window.setupChannel, "function", "window.setupChannel exists for legacy uses");
+  });
+  
+  test("IFRAME channel with assertion", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onsuccess("assertion");
+        equal(winMock.success, "assertion", "assertion made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+  test("IFRAME channel with null assertion", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onsuccess(null);
+        strictEqual(winMock.success, null, "null assertion made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+  test("IFRAME channel with error", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onerror("error");
+        strictEqual(winMock.error, "error", "error made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+});
+
diff --git a/browserid/static/relay/relay.js b/browserid/static/relay/relay.js
index 46212cc2479faef04dc09da5e978b792bbc7c67a..d9c86f4c13d0b4122eb43610ae4662769ce1c06e 100644
--- a/browserid/static/relay/relay.js
+++ b/browserid/static/relay/relay.js
@@ -43,41 +43,114 @@
     log: function() {}
   };
 
-  var ipServer = "https://browserid.org",
-      transaction,
-      origin,
-      chan = Channel.build( {
-        window: window.parent,
+  BrowserID.Relay = (function() {
+    var transaction,
+        origin,
+        channel = Channel,
+        win = window,
+        registerCB;
+
+
+    function init(options) {
+      origin = transaction = registerCB = undefined;
+
+      if(options.window) {
+        win = options.window;
+      }
+
+      if(options.channel) {
+        channel = options.channel;
+      }
+    }
+
+    function open() {
+      var rpc = channel.build({
+        window: win,
         origin: "*",
         scope: "mozid"
-      } );
+      });
 
+      rpc.bind("getVerifiedEmail", function(trans, s) {
+        trans.delayReturn(true);
+        origin = trans.origin;
+        transaction = trans;
 
-  window.register_dialog = function(callback) {
-    // register the dialog, tell the dialog what the origin is.  
-    // Get the origin from the channel binding.
-    callback(origin);
-  };
+        // If the client has run early and already registered its registration 
+        // callback, call it now.
+        if (registerCB) {
+          registerCB(origin, completionCB);  
+        }
+      });
+    }
 
-  window.browserid_relay = function(status, error) {
-      if(error) {
-        errorOut(transaction, error);
+    function registerClient(callback) {
+      // If the origin is ready, call the callback immediately.
+      if (origin) {
+        callback(origin, completionCB);
       }
       else {
-        try {
-          transaction.complete(status);
-        } catch(e) {
-          // The relay function is called a second time after the 
-          // initial success, when the window is closing.
-        }
+        registerCB = callback;
       }
-  };
+    }
+
+    function errorOut(code) {
+      function getVerboseMessage(code) {
+        var msgs = {
+          "canceled": "user canceled selection",
+          "notImplemented": "the user tried to invoke behavior that's not yet implemented",
+          "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers."
+        };
+        var msg = msgs[code];
+        if (!msg) {
+          alert("need verbose message for " + code);
+          msg = "unknown error";
+            }
+        return msg;
+      }
+      transaction.error(code, getVerboseMessage(code));
+    }
+
+    /**
+     * The client calls this to relay a message back to the RP whenever it is 
+     * complete.  This function is passed to the client when the client does 
+     * its registerClient.
+     */
+    function completionCB(status, error) {
+        if(error) {
+          errorOut(error);
+        }
+        else {
+          try {
+            transaction.complete(status);
+          } catch(e) {
+            // The relay function is called a second time after the 
+            // initial success, when the window is closing.
+          }
+        }
+    }
+
+
+    return {
+      /**
+       * Initialize the relay. 
+       * @method init
+       * @param {object} [options] - options used to override window, channel 
+       * for unit testing.
+       */
+      init: init,
 
-  chan.bind("getVerifiedEmail", function(trans, s) {
-    origin = trans.origin;
-    trans.delayReturn(true);
+      /**
+       * Open the relay with the parent window.
+       * @method open
+       */
+      open: open,
 
-    transaction = trans;
-  });
+      /**
+       * Register a client to use the relay
+       * @method registerClient
+       */
+      registerClient: registerClient
+    };
+  }());
 
 }());
diff --git a/browserid/views/relay.ejs b/browserid/views/relay.ejs
index b6f4cc4f87f190231ac5fffe8912ea1a9c778823..1fc38b014dd59ab903ef989de9604ece89eb63c2 100644
--- a/browserid/views/relay.ejs
+++ b/browserid/views/relay.ejs
@@ -13,8 +13,20 @@
       <% } else { %>
         <script type='text/javascript' 
           src='https://browserid.org/dialog/resources/jschannel.js'></script>
+        <script type='text/javascript' 
+          src='https://browserid.org/dialog/resources/browserid.js'></script>
         <script type='text/javascript' 
           src='https://browserid.org/relay/relay.js'></script>
       <% } %>
+
+      <script type="text/javascript">
+        var relay = BrowserID.Relay;
+        relay.init({
+          channel: Channel,
+          window: window.parent
+        });
+
+        relay.open();
+      </script>
 	</body>
 </html>