diff --git a/bin/browserid b/bin/browserid
index c7bc941cf8fd7f75b04541e9220417093582b9f2..225526ab27a818edbc3f7545e0f216b088c5573f 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -25,8 +25,7 @@ heartbeat = require('../lib/heartbeat.js'),
 metrics = require('../lib/metrics.js'),
 logger = require('../lib/logging.js').logger,
 forward = require('../lib/http_forward').forward,
-shutdown = require('../lib/shutdown'),
-views = require('../lib/browserid/views.js');
+shutdown = require('../lib/shutdown');
 
 var app = undefined;
 
@@ -39,7 +38,13 @@ logger.info("browserid server starting up");
 
 // #1 - Setup health check / heartbeat middleware.
 // This is in front of logging on purpose.  see issue #537
-heartbeat.setup(app);
+heartbeat.setup(app, function(cb) {
+  // ping the database to verify we're really healthy.
+  db.ping(function(e) {
+    if (e) logger.error("database ping error: " + e);
+    cb(!e);
+  });
+});
 
 // #2 - logging!  all requests other than __heartbeat__ are logged
 app.use(express.logger({
@@ -99,15 +104,6 @@ app.use(function(req, resp, next) {
   return next();
 });
 
-var static_root = path.join(__dirname, "..", "resources", "static");
-
-app.use(cachify.setup(assets(config.get('supported_languages')),
-        {
-          prefix: config.get('cachify_prefix'),
-          production: config.get('use_minified_resources'),
-          root: static_root
-        }));
-
 // #7 - perform response substitution to support local/dev/beta environments
 // (specifically, this replaces URLs in responses, e.g. https://login.persona.org
 //  with https://login.anosrep.org)
@@ -118,40 +114,13 @@ wsapi.setup({
   forward_writes: urlparse(config.get('dbwriter_url')).validate().normalize().originOnly()
 }, app);
 
-// #9 - handle views for dynamicish content
-views.setup(app);
-
-// #10 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
+// #9 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
 // fake_verification.js.  This is used during testing only and should
 // never be included in a production deployment
 if (process.env['BROWSERID_FAKE_VERIFICATION']) {
   require('../lib/browserid/fake_verification.js').addVerificationWSAPI(app);
 }
 
-// if nothing else has caught this request, serve static files, but ensure
-// that proper vary headers are installed to prevent unwanted caching
-app.use(function(req, res, next) {
-  res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
-  next();
-});
-
-// add 'Access-Control-Allow-Origin' headers to static resources that will be served
-// from the CDN.  We explicitly allow resources served from public_url to access these.
-app.use(function(req, res, next) {
-  res.on('header', function() {
-    res.setHeader("Access-Control-Allow-Origin", config.get('public_url'));
-  });
-  next();
-});
-
-// if we're not serving minified resources (local dev), then we should add
-// .ejs to the mime table so it's properly substituted.  issue #1875
-if (!config.get('use_minified_resources')) {
-  express.static.mime.types['ejs'] = 'text/html';
-}
-
-app.use(express.static(static_root));
-
 // open the databse
 db.open(config.get('database'), function (error) {
   if (error) {
diff --git a/bin/router b/bin/router
index 3f7f1330ddb431d5eb8b16076e0b29b68aefca26..1d333c310362f4d09ab8dd9b2873711b02a85f0d 100755
--- a/bin/router
+++ b/bin/router
@@ -48,10 +48,12 @@ if (!config.get('browserid_url')) {
 // order in which middleware will be invoked as requests are processed.
 
 // #1 - Setup health check / heartbeat middleware.
+// Depends on positive health checks from browserid and static processes
 // This is in front of logging on purpose.  see issue #537
 var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly();
+var static_url = urlparse(config.get('static_url')).validate().normalize().originOnly();
 heartbeat.setup(app, {
-  dependencies: [browserid_url]
+  dependencies: [browserid_url, static_url]
 });
 
 // #2 - logging!  all requests other than __heartbeat__ are logged
@@ -115,16 +117,37 @@ if (config.get('verifier_url')) {
   });
 }
 
-// handle /wsapi writes
-wsapi.setup({
-  router_mode: true,
-  forward_writes: urlparse(config.get('dbwriter_url')).validate().normalize().originOnly()
-}, app);
+// #10 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
+// fake_verification.js.  This is used during testing only and should
+// never be included in a production deployment
+if (process.env['BROWSERID_FAKE_VERIFICATION']) {
+  app.use(function(req, res, next) {
+    if (url.parse(req.url).pathname == '/wsapi/fake_verification') {
+      forward(
+        browserid_url+req.url, req, res,
+        function(err) {
+          if (err) {
+            logger.error("error forwarding request:", err);
+          }
+        });
+    } else {
+      return next();
+    }
+  });
+}
+
+// handle /wsapi reads/writes
+var dbwriter_url = urlparse(config.get('dbwriter_url')).validate().normalize().originOnly();
+
+wsapi.routeSetup(app, {
+  read_url: browserid_url,
+  write_url: dbwriter_url
+});
 
-// Forward all leftover requests to browserid
+//catch-all
 app.use(function(req, res, next) {
   forward(
-    browserid_url+req.url, req, res,
+    static_url+req.url, req, res,
     function(err) {
       if (err) {
         logger.error("error forwarding request:", err);
diff --git a/bin/static b/bin/static
new file mode 100755
index 0000000000000000000000000000000000000000..47af95441d6858387a0bfa09918fef5c124bdc52
--- /dev/null
+++ b/bin/static
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const
+fs = require('fs'),
+path = require('path'),
+url = require('url'),
+http = require('http'),
+urlparse = require('urlparse'),
+express = require('express');
+
+const
+assets = require('../lib/static_resources').all,
+cachify = require('connect-cachify'),
+i18n = require('../lib/i18n.js'),
+wsapi = require('../lib/wsapi.js'),
+httputils = require('../lib/httputils.js'),
+secrets = require('../lib/secrets.js'),
+db = require('../lib/db.js'),
+config = require('../lib/configuration.js'),
+heartbeat = require('../lib/heartbeat.js'),
+metrics = require('../lib/metrics.js'),
+logger = require('../lib/logging.js').logger,
+forward = require('../lib/http_forward').forward,
+shutdown = require('../lib/shutdown'),
+views = require('../lib/static/views.js');
+
+var app = undefined;
+
+app = express.createServer();
+
+logger.info("static starting up");
+
+// Setup health check / heartbeat middleware.
+// This is in front of logging on purpose.  see issue #537
+heartbeat.setup(app);
+
+// logging!  all requests other than __heartbeat__ are logged
+app.use(express.logger({
+  format: config.get('express_log_format'),
+  stream: {
+    write: function(x) {
+      logger.info(typeof x === 'string' ? x.trim() : x);
+    }
+  }
+}));
+
+// #2.1 - localization
+app.use(i18n.abide({
+  supported_languages: config.get('supported_languages'),
+  default_lang: config.get('default_lang'),
+  debug_lang: config.get('debug_lang'),
+  locale_directory: config.get('locale_directory'),
+  disable_locale_check: config.get('disable_locale_check')
+}));
+
+var statsd_config = config.get('statsd');
+if (statsd_config && statsd_config.enabled) {
+  logger_statsd = require("connect-logger-statsd");
+  app.use(logger_statsd({
+    host: statsd_config.hostname || "localhost",
+    port: statsd_config.port || 8125,
+    prefix: statsd_config.prefix || "browserid.static."
+  }));
+}
+// #4 - prevent framing of everything.  content underneath that needs to be
+// framed must explicitly remove the x-frame-options
+app.use(function(req, resp, next) {
+  resp.setHeader('x-frame-options', 'DENY');
+  next();
+});
+
+var static_root = path.join(__dirname, "..", "resources", "static");
+
+// #7 - perform response substitution to support local/dev/beta environments
+// (specifically, this replaces URLs in responses, e.g. https://browserid.org
+//  with https://diresworb.org)
+config.performSubstitution(app);
+
+// #9 - handle views for dynamicish content
+views.setup(app);
+
+app.use(cachify.setup(assets(config.get('supported_languages')),
+        {
+          prefix: config.get('cachify_prefix'),
+          production: config.get('use_minified_resources'),
+          root: static_root,
+        }));
+
+
+// if nothing else has caught this request, serve static files, but ensure
+// that proper vary headers are installed to prevent unwanted caching
+app.use(function(req, res, next) {
+  res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
+  next();
+});
+
+app.use(express.static(static_root));
+
+var bindTo = config.get('bind_to');
+app.listen(bindTo.port, bindTo.host, function() {
+  logger.info("running on http://" + app.address().address + ":" + app.address().port);
+});
diff --git a/config/local.json b/config/local.json
index 0aec32890f91b63109148e09f7bf85cd7877df87..ba653f4feb79cb3112b0265960ce72bd667204d4 100644
--- a/config/local.json
+++ b/config/local.json
@@ -4,6 +4,7 @@
   "dbwriter": { "bind_to": { "port": 10004 } },
   "proxy": { "bind_to": { "port": 10006 } },
   "browserid": { "bind_to": { "port": 10007 } },
+  "static": { "bind_to": { "port": 10010 } },
   "router": { "bind_to": { "port": 10002 } },
   "use_minified_resources": false,
   "database": {
diff --git a/config/production.json b/config/production.json
index 13cbcaf2ed977a1d1dc597c93a2d988335666677..4cb9c533fddfb98d9a6b2ce744f3bc7e2e67895d 100644
--- a/config/production.json
+++ b/config/production.json
@@ -50,7 +50,9 @@
   "dbwriter_url": "http://127.0.0.1:62900",
   "browserid": { "bind_to": { "port": 62700 } },
   "browserid_url": "http://127.0.0.1:62700",
-  "router": { "bind_to": { "port": 63300 } },
+  "static": { "bind_to": { "port": 63400 } },
+  "static_url": "http://127.0.0.1:63400",
+  "router": { "bind_to": { "port": 63300 } }
 
   // set to true to enable the development menu.
   "enable_development_menu": false
diff --git a/lib/configuration.js b/lib/configuration.js
index 1d0bd7a30883b49af16ba955e5068e698a53a367..ef4d97f42e2609563a792783337bbc04f2697663 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -210,6 +210,10 @@ var conf = module.exports = convict({
     format: 'string?',
     env: 'BROWSERID_URL'
   },
+  static_url: {
+    format: 'string?',
+    env: 'STATIC_URL'
+  },
   process_type: 'string',
   email_to_console: 'boolean = false',
   declaration_of_support_timeout_ms: {
diff --git a/lib/email.js b/lib/email.js
index 5afe2fe293f1066bc774eef99f52146ae7502c3e..f87e03e404bee549e7431cc0372d3b43af8f4cd3 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -28,7 +28,7 @@ if (smtp_params && smtp_params.host) {
   }
 }
 
-const TEMPLATE_PATH = path.join(__dirname, "browserid", "email_templates");
+const TEMPLATE_PATH = path.join(__dirname, "static", "email_templates");
 
 // the underbar decorator to allow getext to extract strings 
 function _(str) { return str; }
diff --git a/lib/browserid/email_templates/add.ejs b/lib/static/email_templates/add.ejs
similarity index 100%
rename from lib/browserid/email_templates/add.ejs
rename to lib/static/email_templates/add.ejs
diff --git a/lib/browserid/email_templates/new.ejs b/lib/static/email_templates/new.ejs
similarity index 100%
rename from lib/browserid/email_templates/new.ejs
rename to lib/static/email_templates/new.ejs
diff --git a/lib/browserid/email_templates/reset.ejs b/lib/static/email_templates/reset.ejs
similarity index 100%
rename from lib/browserid/email_templates/reset.ejs
rename to lib/static/email_templates/reset.ejs
diff --git a/lib/browserid/views.js b/lib/static/views.js
similarity index 99%
rename from lib/browserid/views.js
rename to lib/static/views.js
index b26361e2c6a04ff4e381cafe12b62c80132764b1..65a5d64eb9b8d3f599f76563fc5103fd5ba96b38 100644
--- a/lib/browserid/views.js
+++ b/lib/static/views.js
@@ -15,6 +15,8 @@ httputils = require('../httputils.js'),
 etagify = require('etagify'),
 secrets = require('../secrets');
 
+require("jwcrypto/lib/algs/rs");
+
 // all templated content, redirects, and renames are handled here.
 // anything that is not an api, and not static
 const
diff --git a/lib/wsapi.js b/lib/wsapi.js
index e95a6ec4f144d13d0b981d6e3f7a79e0e87eb09b..b9c5b50f7f384ec1f2fe5336cc165b27c4441585 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -50,6 +50,8 @@ if (config.get('public_url').indexOf('https://login.persona.org') !== 0) {
   COOKIE_KEY += "_" + hash.digest('hex').slice(0, 6);
 }
 
+const WSAPI_PREFIX = '/wsapi/';
+
 logger.info('session cookie name is: ' + COOKIE_KEY);
 
 function clearAuthenticatedUser(session) {
@@ -75,7 +77,7 @@ function bcryptPassword(password, cb) {
     statsd.timing('bcrypt.encrypt_time', reqTime);
     cb.apply(null, arguments);
   });
-};
+}
 
 function authenticateSession(session, uid, level, duration_ms) {
   if (['assertion', 'password'].indexOf(level) === -1)
@@ -116,6 +118,30 @@ function databaseDown(res, err) {
   httputils.serviceUnavailable(res, "database unavailable");
 }
 
+function operationFromURL (path) {
+  var purl = url.parse(path);
+  return purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX &&
+          purl.pathname.substr(WSAPI_PREFIX.length) || null;
+}
+
+var APIs;
+function allAPIs () {
+  if (APIs) return APIs;
+
+  APIs = {};
+
+  fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
+    // skip files that don't have a .js suffix or start with a dot
+    if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
+    var operation = f.substr(0, f.length - 3);
+
+    var api = require(path.join(__dirname, 'wsapi', f));
+    APIs[operation] = api;
+  });
+
+  return APIs;
+}
+
 // common functions exported, for use by different api calls
 exports.clearAuthenticatedUser = clearAuthenticatedUser;
 exports.isAuthed = isAuthed;
@@ -127,10 +153,6 @@ exports.langContext = langContext;
 exports.databaseDown = databaseDown;
 
 exports.setup = function(options, app) {
-  const WSAPI_PREFIX = '/wsapi/';
-
-  // all operations that are being forwarded
-  var forwardedOperations = [];
 
   // If externally we're serving content over SSL we can enable things
   // like strict transport security and change the way cookies are set
@@ -162,8 +184,7 @@ exports.setup = function(options, app) {
     // by layers higher up based on cache control headers.
     // the fallout is that all code that interacts with sessions
     // should be under /wsapi
-    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX)
-    {
+    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
       // explicitly disallow caching on all /wsapi calls (issue #294)
       resp.setHeader('Cache-Control', 'no-cache, max-age=0');
 
@@ -189,45 +210,31 @@ exports.setup = function(options, app) {
           return httputils.badRequest(resp, "no such api");
       }
 
-      // if this request is to be forwarded, we will not perform request validation,
-      // cookie parsing, nor body parsing - leaving that up to the process we're forwarding
-      // to.
-      if (-1 !== forwardedOperations.indexOf(operation)) {
-        // queue up the body here on and forward a single unchunked request onto the
-        // writer
-        return bodyParser(req, resp, function() {
-          next();
-        });
-      } else if (options.router_mode) {
-        // skip wsapi request, let browserid middleware handle forwards
-        return next();
-      } else {
-        // this is not a forwarded operation, perform full parsing and validation
-        return cookieParser(req, resp, function() {
-          bodyParser(req, resp, function() {
-            cookieSessionMiddleware(req, resp, function() {
-              // only on POSTs
-              if (req.method === "POST") {
-
-                if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
-                  logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
-                  return httputils.forbidden(resp, "no cookie");
-                }
-
-                // and the token must match what is sent in the post body
-                else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
-                  // if any of these things are false, then we'll block the request
-                  var b = req.body ? req.body.csrf : "<none>";
-                  var s = req.session ? req.session.csrf : "<none>";
-                  logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
-                  return httputils.badRequest(resp, "CSRF violation");
-                }
+      // perform full parsing and validation
+      return cookieParser(req, resp, function() {
+        bodyParser(req, resp, function() {
+          cookieSessionMiddleware(req, resp, function() {
+            // only on POSTs
+            if (req.method === "POST") {
+
+              if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
+                logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
+                return httputils.forbidden(resp, "no cookie");
+              }
+
+              // and the token must match what is sent in the post body
+              else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
+                // if any of these things are false, then we'll block the request
+                var b = req.body ? req.body.csrf : "<none>";
+                var s = req.session ? req.session.csrf : "<none>";
+                logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
+                return httputils.badRequest(resp, "CSRF violation");
               }
-              return next();
-            });
+            }
+            return next();
           });
         });
-      }
+      });
     } else {
       return next();
     }
@@ -243,54 +250,25 @@ exports.setup = function(options, app) {
     if (op.args) {
       str += " - " + op.args.join(", ");
     }
+    if (op.internal) str += ' - internal';
     str += ")";
     logger.debug(str);
   }
 
-  fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
-    // skip files that don't have a .js suffix or start with a dot
-    if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
-    var operation = f.substr(0, f.length - 3);
-
+  var all = allAPIs();
+  Object.keys(all).forEach(function (operation) {
     try {
-      var api = require(path.join(__dirname, 'wsapi', f));
+      var api = all[operation];
 
-      // don't register read apis if we are configured as a writer,
+      // - don't register read apis if we are configured as a writer,
       // with the exception of ping which tests database connection health.
-      if (options.only_write_apis && !api.writes_db &&
-          operation != 'ping') return;
+      // - don't register write apis if we are not configured as a writer
+      if ((options.only_write_apis && !api.writes_db && operation != 'ping') ||
+          (!options.only_write_apis && api.writes_db))
+            return;
 
       wsapis[operation] = api;
 
-      // forward writes if options.forward_writes is defined
-      if (options.forward_writes && wsapis[operation].writes_db &&
-          !wsapis[operation].disallow_forward)
-      {
-        forwardedOperations.push(operation);
-        var forward_url = options.forward_writes + "/wsapi/" + operation;
-        wsapis[operation].process = function(req, res) {
-          forward(forward_url, req, res, function(err) {
-            if (err) {
-              logger.error("error forwarding '"+ operation +
-                           "' request to '" + options.forward_writes + ":" + err);
-              httputils.serverError(res, "internal request forwarding error");
-            }
-          });
-        };
-
-        // XXX: disable validation on forwarded requests
-        // (we cannot perform this validation because we don't parse cookies
-        // nor post bodies on forwarded requests)
-        //
-        // at some point we'll want to improve our cookie parser and
-        // fully validate forwarded requests both at the intermediate
-        // hop (webhead) AND final destination (secure webhead)
-
-        delete api.args; // deleting args will cause arg validation to be skipped
-
-        api.authed = false; // authed=false will prevent us from checking auth status
-      }
-
       // set up the argument validator
       if (api.args) {
         if (!Array.isArray(api.args)) throw "exports.args must be an array of strings";
@@ -309,30 +287,15 @@ exports.setup = function(options, app) {
   // debug output - all supported apis
   logger.debug("WSAPIs:");
   Object.keys(wsapis).forEach(function(api) {
-    if (options.forward_writes && wsapis[api].writes_db) return;
     describeOperation(api, wsapis[api]);
   });
 
-  if (options.forward_writes) {
-    logger.debug("forwarded WSAPIs (to " + options.forward_writes + "):");
-    Object.keys(wsapis).forEach(function(api) {
-      if (wsapis[api].writes_db) {
-        describeOperation(api, wsapis[api]);
-      }
-    });
-  }
-
   app.use(function(req, resp, next) {
     var purl = url.parse(req.url);
 
     if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
       const operation = purl.pathname.substr(WSAPI_PREFIX.length);
 
-      if (options.router_mode && -1 === forwardedOperations.indexOf(operation)) {
-        // skip wsapi request, let browserid middleware handle forwards
-        return next();
-      }
-
       // the fake_verification wsapi is implemented elsewhere.
       if (operation == 'fake_verification') return next();
 
@@ -359,3 +322,46 @@ exports.setup = function(options, app) {
     }
   });
 };
+
+
+exports.routeSetup = function (app, options) {
+  var wsapis = allAPIs();
+
+  app.use(function(req, resp, next) {
+    var operation = operationFromURL(req.url);
+
+    // not a WSAPI request
+    if (!operation) return next();
+
+    var api = wsapis[operation];
+
+    // check to see if the api is known here, before spending more time with
+    // the request.
+    if (!wsapis.hasOwnProperty(operation) ||
+        api.method.toLowerCase() !== req.method.toLowerCase()) {
+      // if the fake verification api is enabled (for load testing),
+      // then let this request fall through
+      if (operation !== 'fake_verification' || !process.env['BROWSERID_FAKE_VERIFICATION'])
+        return httputils.badRequest(resp, "no such api");
+    }
+
+    if (api.internal) {
+        return httputils.notFound(resp);
+    }
+
+    var destination_url = api.writes_db ? options.write_url + "/wsapi/" + operation
+                                        : options.read_url + req.url;
+
+    var cb = function() {
+      forward(
+        destination_url, req, resp,
+        function(err) {
+          if (err) {
+            logger.error("error forwarding request:", err);
+          }
+        });
+    };
+    return express.bodyParser()(req, resp, cb);
+
+  });
+};
diff --git a/lib/wsapi/create_account_with_assertion.js b/lib/wsapi/create_account_with_assertion.js
index 13f96d395fb6b56c7f4fa85adfcbc38472ea48cf..3fdd3f3608037056ab8326767946d58a91ef182e 100644
--- a/lib/wsapi/create_account_with_assertion.js
+++ b/lib/wsapi/create_account_with_assertion.js
@@ -12,7 +12,7 @@ logger = require('../logging.js').logger;
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.disallow_forward = true;
+exports.internal = true;
 exports.args = ['assertion'];
 exports.i18n = false;
 
diff --git a/scripts/run_locally.js b/scripts/run_locally.js
index 346fa7dda7dff3152af234e9f9fef5261e0d01fd..f034d8279d9c2e57c3d09b2154a715232d65fbf7 100755
--- a/scripts/run_locally.js
+++ b/scripts/run_locally.js
@@ -30,6 +30,7 @@ var daemonsToRun = {
   },
   proxy: { },
   browserid: { },
+  static: { },
   router: { }
 };
 
@@ -55,6 +56,7 @@ process.env['BROWSERID_URL'] = 'http://' + HOST + ":10007";
 process.env['VERIFIER_URL'] = 'http://' + HOST + ":10000/verify";
 process.env['KEYSIGNER_URL'] = 'http://' + HOST + ":10003";
 process.env['ROUTER_URL'] = 'http://' + HOST + ":10002";
+process.env['STATIC_URL'] = 'http://' + HOST + ":10010";
 
 process.env['PUBLIC_URL'] = process.env['ROUTER_URL'];
 
diff --git a/tests/heartbeat-test.js b/tests/heartbeat-test.js
index 815f97120003b062da83fe9d898acc0fc16ff471..6dd5341b4b586f303cc43069fdfbb5046ff8a311 100755
--- a/tests/heartbeat-test.js
+++ b/tests/heartbeat-test.js
@@ -114,6 +114,66 @@ suite.addBatch({
   }
 });
 
+// now let's SIGSTOP the static process and verify that the router's
+// deep heartbeat fails within 11s
+suite.addBatch({
+  "stopping the static process": {
+    topic: function() {
+      process.kill(parseInt(process.env['STATIC_PID'], 10), 'SIGSTOP');      
+      this.callback();
+    },
+    "then doing a deep __heartbeat__ on router": {
+      topic: function() {
+        var self = this;
+        var start = new Date();
+        var req = http.get({
+          host: '127.0.0.1',
+          port: 10002,
+          path: '/__heartbeat__?deep=true'
+        }, function(res) {
+          self.callback(null, res.statusCode, start);
+          req.abort();
+        }).on('error', function(e) {
+          self.callback(e, null);
+          req.abort();
+        });
+      },
+      "fails": function(e, code, start) {
+        assert.ok(!e);
+        assert.strictEqual(500, code);
+      },
+      "takes about 5s": function(e, code, start) {
+        assert.ok(!e);
+        var elapsedMS = new Date() - start;
+        assert.ok(3000 < elapsedMS < 7000);
+      },
+      "but upon SIGCONT": {
+        topic: function(e, code) {
+          process.kill(parseInt(process.env['STATIC_PID'], 10), 'SIGCONT');      
+          this.callback();
+        },
+        "a deep heartbeat": {
+          topic: function() {
+            var self = this;
+            var req = http.get(
+              { host: '127.0.0.1', port: 10002, path: '/__heartbeat__?deep=true'},
+              function(res) {
+                self.callback(null, res.statusCode);
+                req.abort();
+              }).on('error', function(e) {
+                self.callback(e, null);
+                req.abort();
+              });
+          },
+          "works": function(err, code) {
+            assert.ok(!err);
+            assert.strictEqual(200, code);
+          }
+        }
+      }
+    }
+  }
+});
 
 start_stop.addShutdownBatches(suite);
 
diff --git a/tests/internal-wsapi-test.js b/tests/internal-wsapi-test.js
new file mode 100644
index 0000000000000000000000000000000000000000..59d04ae2dad6f84e5c89a72d187c12f529e7eac5
--- /dev/null
+++ b/tests/internal-wsapi-test.js
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+const
+assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js');
+
+var suite = vows.describe('internal-wsapi');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+start_stop.addStartupBatches(suite);
+
+suite.addBatch({
+  "requesting to create an account with an assertion": {
+    topic: wsapi.post('/wsapi/create_account_with_assertion', { }),
+    "returns a 404": function(err, r) {
+      assert.strictEqual(r.code, 404);
+    }
+  }
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);