diff --git a/README.md b/README.md
index 78616d91bf808211c18b08f23f717826068e39d7..1c24f1e61f4bba0130f473ca080a79ced7637b32 100644
--- a/README.md
+++ b/README.md
@@ -20,3 +20,13 @@ All of the servers here are based on node.js, and some number of 3rd party node
 1. install required software
 2. run the top level *run.js* script: `node ./run.js`
 3. visit the demo application ('rp') in your web browser (url output on the console at runtime)␁
+
+## Testing
+
+We should start using this:
+
+  https://github.com/LearnBoost/tobi
+
+for integration testing
+
+and straight Vows for unit testing
\ No newline at end of file
diff --git a/browserid/app.js b/browserid/app.js
index e3fc71968311e34b1701a466ecb18b9baa729254..4152448b06b0329edc0f330cbe823a09f861b275 100644
--- a/browserid/app.js
+++ b/browserid/app.js
@@ -33,17 +33,10 @@ function router(app) {
   app.get('/sign_in', internal_redirector('/dialog/index.html'));
   app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html'));
 
-  app.all('/wsapi/:method', function(req, resp, next) {
-      try {
-        wsapi[req.params.method](req, resp);
-      } catch (e) {
-        var errMsg = "oops, error executing wsapi method: " + method + " (" + e.toString() +")";
-        console.log(errMsg);
-        httputils.fourOhFour(response, errMsg);
-      }
-    });
+  // register all the WSAPI handlers
+  wsapi.setup(app);
 
-  app.get('/users/acct\::identity.xml', function(req, resp, next) {
+  app.get('/users/:identity.xml', function(req, resp, next) {
       webfinger.renderUserPage(req.params.identity, function (resultDocument) {
           if (resultDocument === undefined) {
             httputils.fourOhFour(resp, "I don't know anything about: " + req.params.identity + "\n");
@@ -80,9 +73,6 @@ exports.setup = function(server) {
     }
   });
 
-  // add the methods
-  router(server);
-
   // a tweak to get the content type of host-meta correct
   server.use(function(req, resp, next) {
     if (req.url === '/.well-known/host-meta') {
@@ -90,4 +80,7 @@ exports.setup = function(server) {
     }
     next();
   });
+
+  // add the actual URL handlers other than static
+  router(server);
 }
diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js
index f76acfdf374275bbfb7c030442e5de113011c767..9d6b1f257fb581b79bd4d2032d90e8884c9a5d51 100644
--- a/browserid/lib/wsapi.js
+++ b/browserid/lib/wsapi.js
@@ -1,5 +1,7 @@
 // a module which implements the authorities web server api.
-// every export is a function which is a WSAPI method handler
+// it used to be that we stuffed every function in exports.
+// now we're using proper express function registration to deal
+// with HTTP methods and the like, apply middleware, etc.
 
 const db = require('./db.js'),
       url = require('url'),
@@ -33,245 +35,244 @@ function checkAuthed(req, resp) {
   return true;
 }
 
-/* checks to see if an email address is known to the server
- * takes 'email' as a GET argument */
-exports.have_email = function(req, resp) {
-  // get inputs from get data!
-  var email = url.parse(req.url, true).query['email'];
-  db.emailKnown(email, function(known) {
-    httputils.jsonResponse(resp, known);
-  });
-};
-
-/* First half of account creation.  Stages a user account for creation.
- * this involves creating a secret url that must be delivered to the
- * user via their claimed email address.  Upon timeout expiry OR clickthrough
- * the staged user account transitions to a valid user account */
-exports.stage_user = function(req, resp) {
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
-
-  if (!checkParams(getArgs, resp, [ "email", "pass", "pubkey", "site" ])) {
-    return;
-  }
-
-  // bcrypt the password
-  getArgs.hash = bcrypt.encrypt_sync(getArgs.pass, bcrypt.gen_salt_sync(4))
-
-  try {
-    // upon success, stage_user returns a secret (that'll get baked into a url
-    // and given to the user), on failure it throws
-    var secret = db.stageUser(getArgs);
-
-    // store the email being registered in the session data
-    if (!req.session) req.session = {};
-
-    // store inside the session the details of this pending verification
-    req.session.pendingVerification = {
-      email: getArgs.email,
-      hash: getArgs.hash // we must store both email and password to handle the case where
-                         // a user re-creates an account - specifically, registration status
-                         // must ensure the new credentials work to properly verify that
-                         // the user has clicked throught the email link. note, this salted, bcrypted
-                         // representation of a user's password will get thrust into an encrypted cookie
-                         // served over an encrypted (SSL) session.  guten, yah.
-    };
-
-    httputils.jsonResponse(resp, true);
-
-    // let's now kick out a verification email!
-    email.sendVerificationEmail(getArgs.email, getArgs.site, secret);
-
-  } catch(e) {
-    // we should differentiate tween' 400 and 500 here.
-    httputils.badRequest(resp, e.toString());
-  }
-};
-
-exports.registration_status = function(req, resp) {
-  if (!req.session ||
-      (!(typeof req.session.pendingVerification === 'object') &&
-       !(typeof req.session.pendingAddition === 'string')))
-  {
-    httputils.badRequest(
-      resp,
-      "api abuse: registration_status called without a pending email addition/verification");
-    return;
-  }
+function setup(app) {
+  /* checks to see if an email address is known to the server
+   * takes 'email' as a GET argument */
+  app.get('/wsapi/have_email', function(req, resp) {
+      // get inputs from get data!
+      var email = url.parse(req.url, true).query['email'];
+      db.emailKnown(email, function(known) {
+          httputils.jsonResponse(resp, known);
+        });
+    });
+  
+  /* First half of account creation.  Stages a user account for creation.
+   * this involves creating a secret url that must be delivered to the
+   * user via their claimed email address.  Upon timeout expiry OR clickthrough
+   * the staged user account transitions to a valid user account */
+  app.get('/wsapi/stage_user', function(req, resp) {
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      
+      if (!checkParams(getArgs, resp, [ "email", "pass", "pubkey", "site" ])) {
+        return;
+      }
 
-  // Is the current session trying to add an email, or register a new one?
-  if (req.session.pendingAddition) {
-    // this is a pending email addition, it requires authentication
-    if (!checkAuthed(req, resp)) return;
+      // bcrypt the password
+      getArgs.hash = bcrypt.encrypt_sync(getArgs.pass, bcrypt.gen_salt_sync(4));
+        
+      try {
+        // upon success, stage_user returns a secret (that'll get baked into a url
+        // and given to the user), on failure it throws
+        var secret = db.stageUser(getArgs);
+        
+        // store the email being registered in the session data
+        if (!req.session) req.session = {};
+        
+        // store inside the session the details of this pending verification
+        req.session.pendingVerification = {
+          email: getArgs.email,
+          hash: getArgs.hash // we must store both email and password to handle the case where
+          // a user re-creates an account - specifically, registration status
+          // must ensure the new credentials work to properly verify that
+          // the user has clicked throught the email link. note, this salted, bcrypted
+          // representation of a user's password will get thrust into an encrypted cookie
+          // served over an encrypted (SSL) session.  guten, yah.
+        };
+        
+        httputils.jsonResponse(resp, true);
+        
+        // let's now kick out a verification email!
+        email.sendVerificationEmail(getArgs.email, getArgs.site, secret);
+        
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
+      }
+    });
 
-    // check if the currently authenticated user has the email stored under pendingAddition
-    // in their acct.
-    db.emailsBelongToSameAccount(
-      req.session.pendingAddition, req.session.authenticatedUser,
-      function(registered)
-      {
-        if (registered) {
-          delete req.session.pendingAddition;
-          httputils.jsonResponse(resp, "complete");
-        } else {
-          httputils.jsonResponse(resp, "pending");
+  app.get('/wsapi/registration_status', function(req, resp) {
+      if (!req.session ||
+          (!(typeof req.session.pendingVerification === 'object') &&
+           !(typeof req.session.pendingAddition === 'string')))
+        {
+          httputils.badRequest(resp, "api abuse: registration_status called without a pending email addition/verification");
+          return;
         }
-      });
-  }
-  else
-  {
-    // this is a pending registration, let's check if the creds stored on the
-    // session are good yet.
-
-    var v = req.session.pendingVerification;
-    db.checkAuthHash(v.email, v.hash, function(authed) {
-      if (authed) {
-        delete req.session.pendingVerification;
-        req.session.authenticatedUser = v.email;
-        httputils.jsonResponse(resp, "complete");
+      
+      // Is the current session trying to add an email, or register a new one?
+      if (req.session.pendingAddition) {
+        // this is a pending email addition, it requires authentication
+        if (!checkAuthed(req, resp)) return;
+        
+        // check if the currently authenticated user has the email stored under pendingAddition
+        // in their acct.
+        db.emailsBelongToSameAccount(req.session.pendingAddition,
+                                     req.session.authenticatedUser,
+                                     function(registered) {
+                                       if (registered) {
+                                         delete req.session.pendingAddition;
+                                         httputils.jsonResponse(resp, "complete");
+                                       } else {
+                                         httputils.jsonResponse(resp, "pending");
+                                       }
+                                     });
       } else {
-        httputils.jsonResponse(resp, "pending");
+        // this is a pending registration, let's check if the creds stored on the
+        // session are good yet.
+        
+        var v = req.session.pendingVerification;
+        db.checkAuthHash(v.email, v.hash, function(authed) {
+            if (authed) {
+              delete req.session.pendingVerification;
+              req.session.authenticatedUser = v.email;
+              httputils.jsonResponse(resp, "complete");
+            } else {
+              httputils.jsonResponse(resp, "pending");
+            }
+          });
+      }
+    });
+  
+  
+  app.get('/wsapi/authenticate_user', function(req, resp) {
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      
+      if (!checkParams(getArgs, resp, [ "email", "pass" ])) return;
+      
+      db.checkAuth(getArgs.email, getArgs.pass, function(rv) {
+          if (rv) {
+            if (!req.session) req.session = {};
+            req.session.authenticatedUser = getArgs.email;
+          }
+          httputils.jsonResponse(resp, rv);
+        });
+    });
+    
+  // FIXME: need CSRF protection
+  app.get('/wsapi/add_email', function (req, resp) {
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      
+      if (!checkParams(getArgs, resp, [ "email", "pubkey", "site" ])) return;
+      
+      if (!checkAuthed(req, resp)) return;
+      
+      try {
+        // upon success, stage_user returns a secret (that'll get baked into a url
+        // and given to the user), on failure it throws
+        var secret = db.stageEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey);
+        
+        // store the email being added in session data
+        req.session.pendingAddition = getArgs.email;
+        
+        httputils.jsonResponse(resp, true);
+        
+        // let's now kick out a verification email!
+        email.sendVerificationEmail(getArgs.email, getArgs.site, secret);
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
       }
     });
-  }
-};
-
-exports.authenticate_user = function(req, resp) {
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
-
-  if (!checkParams(getArgs, resp, [ "email", "pass" ])) return;
-
-  db.checkAuth(getArgs.email, getArgs.pass, function(rv) {
-    if (rv) {
-      if (!req.session) req.session = {};
-      req.session.authenticatedUser = getArgs.email;
-    }
-    httputils.jsonResponse(resp, rv);
-  });
-};
-
-// need CSRF protection
-
-exports.add_email = function (req, resp) {
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
-
-  if (!checkParams(getArgs, resp, [ "email", "pubkey", "site" ])) return;
-
-  if (!checkAuthed(req, resp)) return;
-
-  try {
-    // upon success, stage_user returns a secret (that'll get baked into a url
-    // and given to the user), on failure it throws
-    var secret = db.stageEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey);
-
-    // store the email being added in session data
-    req.session.pendingAddition = getArgs.email;
-
-    httputils.jsonResponse(resp, true);
-
-    // let's now kick out a verification email!
-    email.sendVerificationEmail(getArgs.email, getArgs.site, secret);
-  } catch(e) {
-    // we should differentiate tween' 400 and 500 here.
-    httputils.badRequest(resp, e.toString());
-  }
-};
-
-exports.remove_email = function(req, resp) {
-  // this should really be POST, but for now I'm having trouble seeing
-  // how to get POST args properly, so it's a GET (Ben).
-  // hmmm, I really want express or some other web framework!
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
-
-  if (!checkParams(getArgs, resp, [ "email"])) return;
-  if (!checkAuthed(req, resp)) return;
-
-  db.removeEmail(req.session.authenticatedUser, getArgs.email, function(error) {
-    if (error) {
-      console.log("error removing email " + getArgs.email);
-      httputils.badRequest(resp, error.toString());
-    } else {
-      httputils.jsonResponse(resp, true);
-    }});
-};
-
-exports.account_cancel = function(req, resp) {
-  // this should really be POST
-  if (!checkAuthed(req, resp)) return;
-
-  db.cancelAccount(req.session.authenticatedUser, function(error) {
-    if (error) {
-      console.log("error cancelling account : " + error.toString());
-      httputils.badRequest(resp, error.toString());
-    } else {
-      httputils.jsonResponse(resp, true);
-    }});
-};
-
-exports.set_key = function (req, resp) {
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
-  if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return;
-  if (!checkAuthed(req, resp)) return;
-  db.addKeyToEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) {
-    httputils.jsonResponse(resp, rv);
-  });
-};
 
-exports.am_authed = function(req,resp) {
-  // if they're authenticated for an email address that we don't know about,
-  // then we should purge the stored cookie
-  if (!isAuthed(req)) {
-    httputils.jsonResponse(resp, false);
-  } else {
-    db.emailKnown(req.session.authenticatedUser, function (known) {
-      if (!known) req.session = {}
-      httputils.jsonResponse(resp, known);
+  app.get('/wsapi/remove_email', function(req, resp) {
+      // this should really be POST, but for now I'm having trouble seeing
+      // how to get POST args properly, so it's a GET (Ben).
+      // hmmm, I really want express or some other web framework!
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      
+      if (!checkParams(getArgs, resp, [ "email"])) return;
+      if (!checkAuthed(req, resp)) return;
+      
+      db.removeEmail(req.session.authenticatedUser, getArgs.email, function(error) {
+          if (error) {
+            console.log("error removing email " + getArgs.email);
+            httputils.badRequest(resp, error.toString());
+          } else {
+            httputils.jsonResponse(resp, true);
+          }});
     });
-  }
-};
 
-exports.logout = function(req,resp) {
-  req.session = {};
-  httputils.jsonResponse(resp, "ok");
-};
+  app.get('/wsapi/account_cancel', function(req, resp) {
+      // this should really be POST
+      if (!checkAuthed(req, resp)) return;
+      
+      db.cancelAccount(req.session.authenticatedUser, function(error) {
+          if (error) {
+            console.log("error cancelling account : " + error.toString());
+            httputils.badRequest(resp, error.toString());
+          } else {
+            httputils.jsonResponse(resp, true);
+          }});
+    });
 
-exports.sync_emails = function(req,resp) {
-  if (!checkAuthed(req, resp)) return;
+  app.get('/wsapi/set_key', function (req, resp) {
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return;
+      if (!checkAuthed(req, resp)) return;
+      db.addKeyToEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) {
+          httputils.jsonResponse(resp, rv);
+        });
+    });
 
-  var requestBody = "";
-  req.on('data', function(str) {
-    requestBody += str;
-  });
-  req.on('end', function() {
-    try {
-      var emails = JSON.parse(requestBody);
-    } catch(e) {
-      httputils.badRequest(resp, "malformed payload: " + e);
-    }
-    db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) {
-      if (err) httputils.serverError(resp, err);
-      else httputils.jsonResponse(resp, syncResponse);
+  app.get('/wsapi/am_authed', function(req,resp) {
+      // if they're authenticated for an email address that we don't know about,
+      // then we should purge the stored cookie
+      if (!isAuthed(req)) {
+        httputils.jsonResponse(resp, false);
+      } else {
+        db.emailKnown(req.session.authenticatedUser, function (known) {
+            if (!known) req.session = {}
+            httputils.jsonResponse(resp, known);
+          });
+      }
     });
-  });
-};
 
-exports.prove_email_ownership = function(req, resp) {
-  var urlobj = url.parse(req.url, true);
-  var getArgs = urlobj.query;
+  app.get('/wsapi/logout', function(req,resp) {
+      req.session = {};
+      httputils.jsonResponse(resp, "ok");
+    });
 
-  // validate inputs
-  if (!checkParams(getArgs, resp, [ "token" ])) return;
+  app.post('/wsapi/sync_emails', function(req,resp) {
+      if (!checkAuthed(req, resp)) return;
+      
+      var requestBody = "";
+      req.on('data', function(str) {
+          requestBody += str;
+        });
+      req.on('end', function() {
+          try {
+            var emails = JSON.parse(requestBody);
+          } catch(e) {
+            httputils.badRequest(resp, "malformed payload: " + e);
+          }
+          db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) {
+              if (err) httputils.serverError(resp, err);
+              else httputils.jsonResponse(resp, syncResponse);
+            });
+        });
+    });
 
-  db.gotVerificationSecret(getArgs.token, function(e) {
-    if (e) {
-      console.log("error completing the verification: " + e);
-      httputils.jsonResponse(resp, false);
-    } else {
-      httputils.jsonResponse(resp, true);
-    }
-  });
+  app.get('/wsapi/prove_email_ownership', function(req, resp) {
+      var urlobj = url.parse(req.url, true);
+      var getArgs = urlobj.query;
+      
+      // validate inputs
+      if (!checkParams(getArgs, resp, [ "token" ])) return;
+      
+      db.gotVerificationSecret(getArgs.token, function(e) {
+          if (e) {
+            console.log("error completing the verification: " + e);
+            httputils.jsonResponse(resp, false);
+          } else {
+            httputils.jsonResponse(resp, true);
+          }
+        });
+    });
 }
+
+exports.setup = setup;
diff --git a/browserid/tests/registration-status-wsapi-test.js b/browserid/tests/registration-status-wsapi-test.js
index 7837e90d21733172b2a5e5fac7ea63f5b38b08f2..df8d28ba630c2331784717429280bbafc33d889d 100755
--- a/browserid/tests/registration-status-wsapi-test.js
+++ b/browserid/tests/registration-status-wsapi-test.js
@@ -8,6 +8,9 @@ const assert = require('assert'),
 
 var suite = vows.describe('registration-status-wsapi');
 
+// FIXME: these tests are probably going to fail after Ben
+// revamps wsapi to be more express-like.
+
 // ever time a new token is sent out, let's update the global
 // var 'token'
 var token = undefined;