diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 54a350162cdea99ed069928217df0c21cf9814b7..8171cb45f80faedd7a842d2a3721caa9dab89e4b 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -85,7 +85,6 @@ Subsequent steps use different software which you might need to install.
 
   * **curl** - used to iniate http requests from the cmd line (to kick the browserid server)
   * **java** - used to minify css
-  * **libsqlite3-dev** - database libraries 
 
 ### 4. Set up post-update hook
 
diff --git a/README.md b/README.md
index e84edefb0d776fd66d4553e07bd87f77352498a3..518808162dd1ff1aad9c10ecd7bd72330778e21b 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,6 @@ Here's the software you'll need installed:
 
 * node.js (>= 0.4.5): http://nodejs.org/
 * npm: http://npmjs.org/
-* sqlite (3) development libraries: http://www.sqlite.org/
 * Several node.js 3rd party libraries - see `package.json` for details
 
 ## Getting started:
diff --git a/browserid/app.js b/browserid/app.js
index 5fdaba92699847e157a04d5fb3ade48caf0a0252..db877ddc8ed2edaf7b4d37e650525da38d1595a1 100644
--- a/browserid/app.js
+++ b/browserid/app.js
@@ -21,6 +21,9 @@ configuration = require('../libs/configuration.js'),
 substitution = require('../libs/substitute.js');
 logging = require("../libs/logging.js");
 
+// open the databse
+db.open(configuration.get('database'));
+
 // looks unused, see run.js
 // const STATIC_DIR = path.join(path.dirname(__dirname), "static");
 const COOKIE_SECRET = secrets.hydrateSecret('cookie_secret', VAR_DIR);
@@ -34,7 +37,7 @@ function internal_redirector(new_url) {
 }
 
 function router(app) {
-  app.set("views", __dirname + '/views'); 
+  app.set("views", __dirname + '/views');
 
   app.set('view options', {
     production: configuration.get('use_minified_resources')
diff --git a/browserid/lib/db.js b/browserid/lib/db.js
index 482fad4c1d92afa2b94ae982959d540a6f880571..56c59253c75fb5667ace08b18166a82aca5a7d30 100644
--- a/browserid/lib/db.js
+++ b/browserid/lib/db.js
@@ -1,391 +1,76 @@
-const
-sqlite = require('sqlite'),
-path = require('path');
-
-var VAR_DIR = path.join(path.dirname(__dirname), "var");
-
-var db = new sqlite.Database();
-
-// a configurable parameter if set immediately after require() of db.js
-exports.dbPath = path.join(VAR_DIR, "authdb.sqlite");
+var driver;
 
 var ready = false;
 var waiting = [];
 
+function checkReady() {
+  if (!ready) throw "database not ready.  did you call open()?";
+}
+
 // async break allow database path to be configured by calling code
 // a touch tricky cause client must set dbPath before releasing
 // control of the runloop
-setTimeout(function() {
-  db.open(exports.dbPath, function (error) {
-    if (error) {
-      console.log("Couldn't open database: " + error);
-      throw error;
-    }
-    db.executeScript(
-      "CREATE TABLE IF NOT EXISTS users  ( id INTEGER PRIMARY KEY, password TEXT );" +
-        "CREATE TABLE IF NOT EXISTS emails ( id INTEGER PRIMARY KEY, user INTEGER, address TEXT UNIQUE );" +
-        "CREATE TABLE IF NOT EXISTS keys   ( id INTEGER PRIMARY KEY, email INTEGER, key TEXT, expires INTEGER )",
-      function (error) {
-        if (error) {
-          throw error;
-        }
-        ready = true;
-        waiting.forEach(function(f) { f() });
-        waiting = [];
-      });
-  });
-}, 0);
-
-// accepts a function that will be invoked once the database is ready for transactions.
-// this hook is important to pause the rest of application startup until async database
-// connection establishment is complete.
-exports.onReady = function(f) {
-  setTimeout(function() {
-    if (ready) f();
-    else waiting.push(f);
-  }, 0);
-};
-
-// XXX: g_staged and g_stagedEmails should be moved into persistent/fast storage.
-
-// half created user accounts (pending email verification)
-// OR
-// half added emails (pending verification)
-var g_staged = {
-};
-
-// an email to secret map for efficient fulfillment of isStaged queries
-var g_stagedEmails = {
-};
-
-function executeTransaction(statements, cb) {
-  function executeTransaction2(statements, cb) {
-    if (statements.length == 0) cb();
-    else {
-      var s = statements.shift();
-      db.execute(s[0], s[1], function(err, rows) {
-        if (err) cb(err);
-        else executeTransaction2(statements, cb);
-      });
-    }
+exports.open = function(cfg, cb) {
+  var driverName = "json";
+  if (cfg && cfg.driver) driverName = cfg.driver;
+  try {
+    driver = require('./db_' + driverName + '.js');
+  } catch(e) {
+    var msg = "FATAL: couldn't find database driver: " + driverName;
+    console.log(msg);
+    throw msg + ": " + e.toString();
   }
 
-  db.execute('BEGIN', function(err, rows) {
-    executeTransaction2(statements, function(err) {
-      if (err) cb(err);
-      else db.execute('COMMIT', function(err, rows) {
-        cb(err);
-      });
-    });
-  });
-}
-
-function emailToUserID(email, cb) {
-  db.execute(
-    'SELECT users.id FROM emails, users WHERE emails.address = ? AND users.id == emails.user',
-    [ email ],
-    function (err, rows) {
-      if (rows && rows.length == 1) {
-        cb(rows[0].id);
-      } else {
-        if (err) console.log("database error: " + err);
-        cb(undefined);
+  driver.open(cfg, function(error) {
+    if (error) {
+      if (cb) cb(error);
+      else {
+        console.log("ERROR:" + error);
+        process.exit(1);
       }
-    });
-}
-
-exports.emailKnown = function(email, cb) {
-  db.execute(
-    "SELECT id FROM emails WHERE address = ?",
-    [ email ],
-    function(error, rows) {
-      cb(rows.length > 0);
-    });
-};
-
-// XXX: should be moved to async.
-exports.isStaged = function(email) {
-  return g_stagedEmails.hasOwnProperty(email);
-};
-
-function generateSecret() {
-  var str = "";
-  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-  for (var i=0; i < 48; i++) {
-    str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
-  }
-  return str;
-}
-
-function addEmailToAccount(existing_email, email, pubkey, cb) {
-  emailToUserID(existing_email, function(userID) {
-    if (userID == undefined) {
-      cb("no such email: " + existing_email, undefined);
     } else {
-      executeTransaction([
-        [ "INSERT INTO emails (user, address) VALUES(?,?)", [ userID, email ] ],
-        [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)",
-          [ pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
-        ]
-      ], function (error) {
-        if (error) cb(error);
-        else cb();
-      });
+      ready = true;
+      waiting.forEach(function(f) { f() });
+      waiting = [];
+      if (cb) cb();
     }
   });
-}
-
-exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
-  emailToUserID(lhs, function(lhs_uid) {
-    emailToUserID(rhs, function(rhs_uid) {
-      cb(lhs_uid === rhs_uid);
-    }, function (error) {
-      cb(false);
-    });
-  }, function (error) {
-    cb(false);
-  });
 };
 
-exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
-  emailToUserID(existing_email, function(userID) {
-    if (userID == undefined) {
-      cb("no such email: " + existing_email, undefined);
-      return;
-    }
 
-    db.execute("SELECT emails.id FROM emails,users WHERE users.id = ? AND emails.address = ? AND emails.user = users.id",
-               [ userID, email ],
-               function(err, rows) {
-                 if (err || rows.length != 1) {
-                   cb(err);
-                   return;
-                 }
-                 executeTransaction([
-                   [ "INSERT INTO keys (email, key, expires) VALUES(?,?,?)",
-                     [ rows[0].id, pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
-                   ]
-                 ], function (error) {
-                   if (error) cb(error);
-                   else cb();
-                 });
-               });
+exports.close = function(cb) {
+  driver.close(function(err) {
+    ready = false;
+    cb(err);
   });
-}
-
-/* takes an argument object including email, password hash, and pubkey. */
-exports.stageUser = function(obj) {
-  var secret = generateSecret();
-
-  // overwrite previously staged users
-  g_staged[secret] = {
-    type: "add_account",
-    email: obj.email,
-    pubkey: obj.pubkey,
-    pass: obj.hash
-  };
-
-  g_stagedEmails[obj.email] = secret;
-  return secret;
 };
 
-/* takes an argument object including email, pass, and pubkey. */
-// XXX: change to async
-exports.stageEmail = function(existing_email, new_email, pubkey) {
-  var secret = generateSecret();
-  // overwrite previously staged users
-  g_staged[secret] = {
-    type: "add_email",
-    existing_email: existing_email,
-    email: new_email,
-    pubkey: pubkey
-  };
-  g_stagedEmails[new_email] = secret;
-  return secret;
-};
-
-/* invoked when a user clicks on a verification URL in their email */
-exports.gotVerificationSecret = function(secret, cb) {
-  if (!g_staged.hasOwnProperty(secret)) return cb("unknown secret");
-
-  // simply move from staged over to the emails "database"
-  var o = g_staged[secret];
-  delete g_staged[secret];
-  delete g_stagedEmails[o.email];
-  if (o.type === 'add_account') {
-    exports.emailKnown(o.email, function(known) {
-      function createAccount() {
-        executeTransaction([
-          [ "INSERT INTO users (password) VALUES(?)", [ o.pass ] ] ,
-          [ "INSERT INTO emails (user, address) VALUES(last_insert_rowid(),?)", [ o.email ] ],
-          [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)",
-            [ o.pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ]
-          ]
-        ], function (error) {
-          if (error) cb(error);
-          else cb();
-        });
-      }
-
-      // if this email address is known and a user has completed a re-verification of this email
-      // address, remove the email from the old account that it was associated with, and then
-      // create a brand new account with only this email.
-      // NOTE: this might be sub-optimal, but it's a dead simple approach that mitigates many attacks
-      // and gives us reasonable behavior (without explicitly supporting) in the face of shared email
-      // addresses.
-      if (known) {
-        exports.removeEmail(o.email, o.email, function (err) {
-          if (err) cb(err);
-          else createAccount();
-        });
-      } else {
-        createAccount();
-      }
-    });
-  } else if (o.type === 'add_email') {
-    exports.emailKnown(o.email, function(known) {
-      function addIt() {
-        addEmailToAccount(o.existing_email, o.email, o.pubkey, cb);
-      }
-      if (known) {
-        exports.removeEmail(o.email, o.email, function (err) {
-          if (err) cb(err);
-          else addIt();
-        });
-      } else {
-        addIt();
-      }
-    });
-  } else {
-    cb("internal error");
-  }
-};
-
-// check authentication credentials for a given email address.  This will invoke the
-// users callback with the authentication (password/hash/whatever - the database layer
-// doesn't care).  callback will be passed undefined if email cannot be found
-exports.checkAuth = function(email, cb) {
-  db.execute("SELECT users.password FROM emails, users WHERE users.id = emails.user AND emails.address = ?",
-             [ email ],
-             function (error, rows) {
-               cb(rows.length !== 1 ? undefined : rows[0].password);
-             });
+// accepts a function that will be invoked once the database is ready for transactions.
+// this hook is important to pause the rest of application startup until async database
+// connection establishment is complete.
+exports.onReady = function(f) {
+  setTimeout(function() {
+    if (ready) f();
+    else waiting.push(f);
+  }, 0);
 };
 
-function emailHasPubkey(email, pubkey, cb) {
-  db.execute(
-    'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id AND keys.key = ?',
-    [ email, pubkey ],
-    function(err, rows) {
-      cb(rows.length === 1);
-    });
-}
-
-/* a high level operation that attempts to sync a client's view with that of the
- * server.  email is the identity of the authenticated channel with the user,
- * identities is a map of email -> pubkey.
- * We'll return an object that expresses three different types of information:
- * there are several things we need to express:
- * 1. emails that the client knows about but we do not
- * 2. emails that we know about and the client does not
- * 3. emails that we both know about but who need to be re-keyed
- * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action
- *       is the same (regen keypair and tell us about it).
- */
-exports.getSyncResponse = function(email, identities, cb) {
-  var respBody = {
-    unknown_emails: [ ],
-    key_refresh: [ ]
+[
+  'emailKnown',
+  'isStaged',
+  'emailsBelongToSameAccount',
+  'addKeyToEmail',
+  'stageUser',
+  'stageEmail',
+  'gotVerificationSecret',
+  'checkAuth',
+  'getSyncResponse',
+  'pubkeysForEmail',
+  'removeEmail',
+  'cancelAccount'
+].forEach(function(fn) {
+  exports[fn] = function() {
+    checkReady();
+    driver[fn].apply(undefined, arguments);
   };
-
-  // get the user id associated with this account
-  emailToUserID(email, function(userID) {
-    if (userID === undefined) {
-      cb("no such email: " + email);
-      return;
-    }
-    db.execute(
-      'SELECT address FROM emails WHERE ? = user',
-      [ userID ],
-      function (err, rows) {
-        if (err) cb(err);
-        else {
-          var emails = [ ];
-          var keysToCheck = [ ];
-          for (var i = 0; i < rows.length; i++) emails.push(rows[i].address);
-
-          // #1
-          for (var e in identities) {
-            if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
-            else keysToCheck.push(e);
-          }
-
-          // #2
-          for (var e in emails) {
-            e = emails[e];
-            if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
-            
-          }
-
-          // #3 -- yes, this is sub-optimal in terms of performance.  when we
-          // move away from public keys this will be unnec.
-          if (keysToCheck.length) {
-            var checked = 0;
-            keysToCheck.forEach(function(e) {
-              emailHasPubkey(e, identities[e], function(v) {
-                checked++;
-                if (!v) respBody.key_refresh.push(e);
-                if (checked === keysToCheck.length) {
-                  cb(undefined, respBody);
-                }
-              });
-            });
-          } else {
-            cb(undefined, respBody);
-          }
-        }
-      });
-  });
-};
-
-// get all public keys associated with an email address
-exports.pubkeysForEmail = function(identity, cb) {
-  db.execute(
-    'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id',
-    [ identity ],
-    function(err, rows) {
-      var keys = undefined;
-      if (!err && rows && rows.length) {
-        keys = [ ];
-        for (var i = 0; i < rows.length; i++) keys.push(rows[i].key);
-      }
-      cb(keys);
-    });
-};
-
-exports.removeEmail = function(authenticated_email, email, cb) {
-  // figure out the user, and remove Email only from addressed
-  // linked to the authenticated email address
-  emailToUserID(authenticated_email, function(user_id) {
-    executeTransaction([
-      [ "delete from emails where emails.address = ? and user = ?", [ email,user_id ] ] ,
-      [ "delete from keys where email in (select address from emails where emails.address = ? and user = ?)", [ email,user_id ] ],
-    ], function (error) {
-      if (error) cb(error);
-      else cb();
-    });
-  });
-};
-
-exports.cancelAccount = function(authenticated_email, cb) {
-  emailToUserID(authenticated_email, function(user_id) {
-    executeTransaction([
-      [ "delete from emails where user = ?", [ user_id ] ] ,
-      [ "delete from keys where email in (select address from emails where user = ?)", [ user_id ] ],
-      [ "delete from users where id = ?", [ user_id ] ],
-    ], function (error) {
-      if (error) cb(error);
-      else cb();
-    });
-  });
-};
+});
diff --git a/browserid/lib/db_json.js b/browserid/lib/db_json.js
new file mode 100644
index 0000000000000000000000000000000000000000..af01b2c954305b256de6979577ad2714bff29e7e
--- /dev/null
+++ b/browserid/lib/db_json.js
@@ -0,0 +1,322 @@
+/* db_json is a json database driver.  It is designed for use in
+ * local development, is intended to be extremely easy to maintain,
+ * have minimal dependencies on 3rd party libraries, and we could
+ * care less if it performs well with more than 10 or so users.
+ */
+const
+path = require('path'),
+fs = require('fs'),
+secrets = require('./secrets'),
+jsel = require('JSONSelect');
+
+// a little alias for stringify
+const ESC = JSON.stringify;
+
+var VAR_DIR = path.join(path.dirname(__dirname), "var");
+
+var dbPath = path.join(VAR_DIR, "authdb.json");
+
+/* The JSON database. The structure is thus:
+ *  [
+ *    {
+ *      password: "somepass",
+ *      emails: [
+ *        {
+ *          address: "lloyd@hilaiel.com",
+ *          keys: [
+ *            {
+ *              key: "SOMESTRINGOFTEXT",
+ *              expires: 1231541615125
+ *            }
+ *          ]
+ *        }
+ *      ]
+ *    }
+ *  ]
+ */
+
+var db = [];
+var stagedEmails = { };
+var staged = { };
+
+function flush() {
+  fs.writeFileSync(dbPath, JSON.stringify(db));
+}
+
+// when should a key created right now expire?
+function getExpiryTime() {
+  return ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000));
+}
+
+exports.open = function(cfg, cb) {
+  if (cfg && cfg.path) dbPath = cfg.path;
+  try {
+    db = JSON.parse(fs.readFileSync(dbPath));
+  } catch(e) {
+  }
+
+  setTimeout(cb, 0);
+};
+
+exports.close = function(cb) {
+  flush();
+  setTimeout(cb, 0);
+};
+
+exports.emailKnown = function(email, cb) {
+  var m = jsel.match(".address:val(" + ESC(email) + ")", db);
+  setTimeout(function() { cb(m.length > 0) }, 0);
+};
+
+exports.isStaged = function(email, cb) {
+  if (cb) {
+    setTimeout(function() {
+      cb(stagedEmails.hasOwnProperty(email));
+    }, 0);
+  }
+};
+
+exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
+  emailToUserID(lhs, function(lhs_uid) {
+    emailToUserID(rhs, function(rhs_uid) {
+      cb(lhs_uid === rhs_uid);
+    }, function (error) {
+      cb(false);
+    });
+  }, function (error) {
+    cb(false);
+  });
+};
+
+function addEmailToAccount(existing_email, email, pubkey, cb) {
+  emailToUserID(existing_email, function(userID) {
+    if (userID == undefined) {
+      cb("no such email: " + existing_email, undefined);
+    } else {
+      db[userID].emails.push({
+        address: email,
+        keys: [
+          {
+            key: pubkey,
+            expires: getExpiryTime()
+          }
+        ]
+      });
+      flush();
+      cb();
+    }
+  });
+}
+
+exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
+  emailToUserID(existing_email, function(userID) {
+    if (userID == undefined) {
+      cb("no such email: " + existing_email, undefined);
+      return;
+    }
+
+    if (!(db[userID].emails)) {
+      db[userID].emails = [ ];
+    }
+
+    var m = jsel.match("object:has(.address:val(" + ESC(email) + ")) > .keys", db[userID].emails);
+
+    var kobj = {
+      key: pubkey,
+      expires: getExpiryTime()
+    };
+
+    if (m.length) {
+      m[0].push(kobj);
+    } else {
+      db[userID].emails.push({
+        address: email,
+        keys: [ kobj ]
+      });
+    }
+
+    flush();
+    if (cb) setTimeout(function() { cb(); }, 0);
+  });
+}
+
+exports.stageUser = function(obj, cb) {
+  var secret = secrets.generate(48);
+
+  // overwrite previously staged users
+  staged[secret] = {
+    type: "add_account",
+    email: obj.email,
+    pubkey: obj.pubkey,
+    pass: obj.hash
+  };
+
+  stagedEmails[obj.email] = secret;
+  setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+  var secret = secrets.generate(48);
+  // overwrite previously staged users
+  staged[secret] = {
+    type: "add_email",
+    existing_email: existing_email,
+    email: new_email,
+    pubkey: pubkey
+  };
+  stagedEmails[new_email] = secret;
+  setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.gotVerificationSecret = function(secret, cb) {
+  if (!staged.hasOwnProperty(secret)) return cb("unknown secret");
+
+  // simply move from staged over to the emails "database"
+  var o = staged[secret];
+  delete staged[secret];
+  delete stagedEmails[o.email];
+  if (o.type === 'add_account') {
+    exports.emailKnown(o.email, function(known) {
+      function createAccount() {
+        db.push({
+          password: o.pass,
+          emails: [
+            {
+              address: o.email,
+              keys: [ {
+                key: o.pubkey,
+                expires: getExpiryTime(),
+              } ]
+            }
+          ]
+        });
+        flush();
+        cb();
+      }
+
+      // if this email address is known and a user has completed a re-verification of this email
+      // address, remove the email from the old account that it was associated with, and then
+      // create a brand new account with only this email.
+      // NOTE: this might be sub-optimal, but it's a dead simple approach that mitigates many attacks
+      // and gives us reasonable behavior (without explicitly supporting) in the face of shared email
+      // addresses.
+
+      if (known) {
+        exports.removeEmail(o.email, o.email, function (err) {
+          if (err) cb(err);
+          else createAccount();
+        });
+      } else {
+        createAccount();
+      }
+    });
+  } else if (o.type === 'add_email') {
+    exports.emailKnown(o.email, function(known) {
+      function addIt() {
+        addEmailToAccount(o.existing_email, o.email, o.pubkey, cb);
+      }
+      if (known) {
+        exports.removeEmail(o.email, o.email, function (err) {
+          if (err) cb(err);
+          else addIt();
+        });
+      } else {
+        addIt();
+      }
+    });
+  } else {
+    cb("internal error");
+  }
+};
+
+exports.checkAuth = function(email, cb) {
+  var m = jsel.match(":root > object:has(.address:val(" + ESC(email) + ")) > .password", db);
+  if (m.length === 0) m = undefined;
+  else m = m[0];
+  setTimeout(function() { cb(m) }, 0);
+};
+
+function emailToUserID(email, cb) {
+  var id = undefined;
+
+  for (var i = 0; i < db.length; i++) {
+    if (jsel.match(".address:val(" + JSON.stringify(email) + ")", db[i]).length) {
+      id = i;
+      break;
+    }
+    if (id !== undefined) break;
+  }
+
+  setTimeout(function() { cb(id); }, 0);
+}
+
+exports.getSyncResponse = function(email, identities, cb) {
+  var respBody = {
+    unknown_emails: [ ],
+    key_refresh: [ ]
+  };
+
+  // get the user id associated with this account
+  emailToUserID(email, function(userID) {
+    if (userID === undefined) {
+      cb("no such email: " + email);
+      return;
+    }
+    var emails = jsel.match(".address", db[userID]);
+    var keysToCheck = [ ];
+
+    // #1 emails that the client knows about but we do not
+    for (var e in identities) {
+      if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
+      else keysToCheck.push(e);
+    }
+
+    // #2 emails that we know about and the client does not
+    for (var e in emails) {
+      e = emails[e];
+      if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
+    }
+
+    // #3 emails that we both know about but who need to be re-keyed
+    if (keysToCheck.length) {
+      var checked = 0;
+      keysToCheck.forEach(function(e) {
+        if (!jsel.match(".key:val(" + ESC(identities[e]) + ")", db[userID]).length)
+          respBody.key_refresh.push(e);
+        checked++;
+        if (checked === keysToCheck.length) cb(undefined, respBody);
+      });
+    } else {
+      cb(undefined, respBody);
+    }
+  });
+};
+
+exports.pubkeysForEmail = function(identity, cb) {
+  var m = jsel.match(".emails object:has(.address:val(" + ESC(identity)+ ")) .key", db);
+  setTimeout(function() { cb(m); }, 0);
+};
+
+exports.removeEmail = function(authenticated_email, email, cb) {
+  var m = jsel.match(":root > object:has(.address:val("+ESC(authenticated_email)+")):has(.address:val("+ESC(email)+")) .emails", db);
+
+  if (m.length) {
+    var emails = m[0];
+    for (var i = 0; i < emails.length; i++) {
+      if (emails[i].address === email) {
+        emails.splice(i, 1);
+        break;
+      }
+    }
+  }
+
+  setTimeout(function() { cb(); }, 0);
+};
+
+exports.cancelAccount = function(authenticated_email, cb) {
+  emailToUserID(authenticated_email, function(user_id) {
+    db.splice(user_id, 1);
+    flush();
+    cb();
+  });
+};
diff --git a/browserid/lib/db_mysql.js b/browserid/lib/db_mysql.js
new file mode 100644
index 0000000000000000000000000000000000000000..eec4eefd383006fb7e5f0c944a40d4d18c071b59
--- /dev/null
+++ b/browserid/lib/db_mysql.js
@@ -0,0 +1,422 @@
+/* This is a mysql driver for the browserid server.  It maps the data
+ * storage requirements of browserid onto a relational schema.  This
+ * driver is intended to be fast and scalable.
+ */
+
+/*
+ * The Schema:
+ *
+ *    +--- user ------+       +--- email ----+        +--- pubkey -----+
+ *    |*int id        | <-\   |*int id       | <-\    |*int id         |
+ *    | string passwd |    \- |*int user     |    \-- |*int email      |
+ *    +---------------+       |*string address        | string pubkey  |
+ *                            +--------------+        | int expires    |
+ *                                                    +----------------+
+ *
+ *
+ *    +------ staged ----------+
+ *    |*string secret          |
+ *    | bool new_acct          |
+ *    | string existing        |
+ *    |*string email           |
+ *    | string pubkey          |
+ *    | string passwd          |
+ *    | timestamp ts           |
+ *    +------------------------+
+ */
+
+const
+mysql = require('mysql'),
+secrets = require('./secrets'),
+logger = require('../../libs/logging.js');
+
+var client = undefined;
+
+// may get defined at open() time causing a database to be dropped upon connection closing.
+var drop_on_close = undefined;
+
+const schemas = [
+  "CREATE TABLE IF NOT EXISTS user   ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );",
+  "CREATE TABLE IF NOT EXISTS email  ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, address VARCHAR(255) UNIQUE, INDEX(address) );",
+  "CREATE TABLE IF NOT EXISTS pubkey ( id INTEGER AUTO_INCREMENT PRIMARY KEY, email INTEGER, content TEXT, expiry DATETIME );",
+  "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), pubkey TEXT, passwd VARCHAR(64), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
+];
+
+// log an unexpected database error
+function logUnexpectedError(detail) {
+  // first, get line number of callee
+  var where;
+  try { dne; } catch (e) { where = e.stack.split('\n')[2].trim(); };
+  // now log it!
+  logger.log('db', { type: "unexpected", message: "unexpected database failure", detail: detail, where: where });
+}
+
+// open & create the mysql database
+exports.open = function(cfg, cb) {
+  if (client) throw "database is already open!";
+  client = new mysql.Client();
+  // mysql config requires
+  const defParams = {
+    host: '127.0.0.1',
+    port: "3306",
+    user: 'test',
+    password: 'pass',
+    unit_test: false
+  };
+
+  Object.keys(defParams).forEach(function(param) {
+    client[param] = cfg[param] ? cfg.param : defParams[param];
+  });
+
+  // let's figure out the database name
+  var database = cfg.database;
+  if (!database) database = "browserid";
+  if (cfg.unit_test) {
+    database += "_" + secrets.generate(8);
+    drop_on_close = database;
+  }
+
+  client.connect(function(error) {
+    if (error) {
+      logUnexpectedError(error);
+      cb(error);
+    } else {
+      // now create the databse
+      client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
+        if (err) {
+          logUnexpectedError(err);
+          cb(err);
+          return;
+        }
+        client.useDatabase(database, function(err) {
+          if (err) {
+            logUnexpectedError(err);
+            cb(err);
+            return;
+          }
+
+          // now create tables
+          function createNextTable(i) {
+            if (i < schemas.length) {
+              client.query(schemas[i], function(err) {
+                if (err) {
+                  logUnexpectedError(err);
+                  cb(err);
+                } else {
+                  createNextTable(i+1);
+                }
+              });
+            } else {
+              cb();
+            }
+          }
+          createNextTable(0);
+        });
+      });
+    }
+  });
+};
+
+exports.close = function(cb) {
+  function endConn() {
+    client.end(function(err) {
+      client = undefined;
+      if (err) logUnexpectedError(err);
+      if (cb) cb(err);
+    });
+  }
+  // when unit_test is specified at open time, we use a temporary database,
+  // and clean it up upon close.
+  if (drop_on_close) {
+    client.query("DROP DATABASE " + drop_on_close, function() {
+      endConn();
+    });
+  } else {
+    endConn();
+  }
+};
+
+exports.emailKnown = function(email, cb) {
+  client.query(
+    "SELECT COUNT(*) as N FROM email WHERE address = ?", [ email ],
+    function(err, rows) {
+      if (err) logUnexpectedError(err);
+      cb(rows && rows.length > 0 && rows[0].N > 0);
+    }
+  );
+}
+
+exports.isStaged = function(email, cb) {
+  client.query(
+    "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ],
+    function(err, rows) {
+      if (err) logUnexpectedError(err);
+      cb(rows && rows.length > 0 && rows[0].N > 0);
+    }
+  );
+}
+
+exports.stageUser = function(obj, cb) {
+  var secret = secrets.generate(48);
+  // overwrite previously staged users
+  client.query('INSERT INTO staged (secret, new_acct, email, pubkey, passwd) VALUES(?,TRUE,?,?,?) ' +
+               'ON DUPLICATE KEY UPDATE secret=?, existing="", new_acct=TRUE, pubkey=?, passwd=?',
+               [ secret, obj.email, obj.pubkey, obj.hash, secret, obj.pubkey, obj.hash],
+               function(err) {
+                 if (err) {
+                   logUnexpectedError(err);
+                   cb(undefined, err);
+                 } else cb(secret);
+               });
+}
+
+exports.gotVerificationSecret = function(secret, cb) {
+  client.query(
+    "SELECT * FROM staged WHERE secret = ?", [ secret ],
+    function(err, rows) {
+      if (err) {
+        logUnexpectedError(err);
+        cb(err);
+      } else if (rows.length === 0) cb("unknown secret");
+      else {
+        var o = rows[0];
+
+        function addEmailAndPubkey(userID) {
+          client.query(
+            "INSERT INTO email(user, address) VALUES(?, ?)",
+            [ userID, o.email ],
+            function(err, info) {
+              if (err) { logUnexpectedError(err); cb(err); return; }
+              addKeyToEmailRecord(info.insertId, o.pubkey, cb);
+            });
+        }
+
+        // delete the record
+        client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]);
+
+        if (o.new_acct) {
+          // we're creating a new account, add appropriate entries into user, email, and pubkey.
+          client.query(
+            "INSERT INTO user(passwd) VALUES(?)",
+            [ o.passwd ],
+            function(err, info) {
+              if (err) { logUnexpectedError(err); cb(err); return; }
+              addEmailAndPubkey(info.insertId);
+            });
+        } else {
+          // we're adding an email address to an existing user account.  add appropriate entries into email and
+          // pubkey
+          client.query(
+            "SELECT user FROM email WHERE address = ?", [ o.existing ],
+            function(err, rows) {
+              if (err) { logUnexpectedError(err); cb(err); }
+              else if (rows.length === 0) cb("cannot find email address: " + o.existing);
+              else {
+                addEmailAndPubkey(rows[0].user);
+              }
+            });
+        }
+      }
+    }
+  );
+}
+
+exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
+  client.query(
+    'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ( SELECT user FROM email WHERE address = ? );',
+    [ lhs, rhs ],
+    function (err, rows) {
+      if (err) cb(false);
+      else cb(rows.length === 1 && rows[0].n === 1);
+    });
+}
+
+function addKeyToEmailRecord(emailId, pubkey, cb) {
+  client.query(
+    // XXX: 2 weeks is wrong, but then so is keypairs.
+    "INSERT INTO pubkey(email, content, expiry) VALUES(?, ?, DATE_ADD(NOW(), INTERVAL 2 WEEK))",
+    [ emailId, pubkey ],
+    function(err, info) {
+      if (err) logUnexpectedError(err);
+      // smash null into undefined.
+      cb(err ? err : undefined);
+    });
+}
+
+exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
+  // this function will NOT add a new email address to a user record.  The only
+  // way that happens is when a verification secret is provided to us.  Limiting
+  // the code paths that result in us concluding that a user owns an email address
+  // is a Good Thing.
+  exports.emailsBelongToSameAccount(existing_email, email, function(ok) {
+    if (!ok) {
+      cb("authenticated user doesn't have permission to add a public key to " + email);
+      return;
+    }
+
+    // now we know that the user has permission to add a key.
+    client.query(
+      "SELECT id FROM email WHERE address = ?", [ email ],
+      function(err, rows) {
+        if (err) { logUnexpectedError(err); cb(err); }
+        else if (rows.length === 0) cb("cannot find email address: " + email);
+        else {
+          addKeyToEmailRecord(rows[0].id, pubkey, cb);
+        }
+      });
+  });
+}
+
+exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+  var secret = secrets.generate(48);
+  // overwrite previously staged users
+  client.query('INSERT INTO staged (secret, new_acct, existing, email, pubkey) VALUES(?,FALSE,?,?,?) ' +
+               'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE, pubkey=?, passwd=""',
+               [ secret, existing_email, new_email, pubkey, secret, existing_email, pubkey],
+               function(err) {
+                 if (err) {
+                   logUnexpectedError(err);
+                   cb(undefined, err);
+                 }
+                 else cb(secret);
+               });
+}
+
+exports.checkAuth = function(email, cb) {
+  client.query(
+    'SELECT passwd FROM user WHERE id = ( SELECT user FROM email WHERE address = ? )',
+    [ email ],
+    function (err, rows) {
+      if (err) logUnexpectedError(err);
+      cb((rows && rows.length == 1) ? rows[0].passwd : undefined);
+    });
+}
+
+function emailHasPubkey(email, pubkey, cb) {
+  client.query(
+    'SELECT pubkey.content FROM pubkey, email WHERE email.address = ? AND pubkey.email = email.id AND pubkey.content = ?',
+    [ email, pubkey ],
+    function(err, rows) {
+      if (err) logUnexpectedError(err);
+      cb(rows && rows.length === 1);
+    });
+}
+
+/* a high level operation that attempts to sync a client's view with that of the
+ * server.  email is the identity of the authenticated channel with the user,
+ * identities is a map of email -> pubkey.
+ * We'll return an object that expresses three different types of information:
+ * there are several things we need to express:
+ * 1. emails that the client knows about but we do not
+ * 2. emails that we know about and the client does not
+ * 3. emails that we both know about but who need to be re-keyed
+ * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action
+ *       is the same (regen keypair and tell us about it).
+ */
+exports.getSyncResponse = function(email, identities, cb) {
+  var respBody = {
+    unknown_emails: [ ],
+    key_refresh: [ ]
+  };
+
+  client.query(
+    'SELECT address FROM email WHERE user = ( SELECT user FROM email WHERE address = ? ) ',
+      [ email ],
+      function (err, rows) {
+        if (err) cb(err);
+        else {
+          var emails = [ ];
+          var keysToCheck = [ ];
+          for (var i = 0; i < rows.length; i++) emails.push(rows[i].address);
+
+          // #1
+          for (var e in identities) {
+            if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e);
+            else keysToCheck.push(e);
+          }
+
+          // #2
+          for (var e in emails) {
+            e = emails[e];
+            if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e);
+          }
+
+          // #3 -- yes, this is sub-optimal in terms of performance.  when we
+          // move away from public keys this will be unnec.
+          if (keysToCheck.length) {
+            var checked = 0;
+            keysToCheck.forEach(function(e) {
+              emailHasPubkey(e, identities[e], function(v) {
+                checked++;
+                if (!v) respBody.key_refresh.push(e);
+                if (checked === keysToCheck.length) {
+                  cb(undefined, respBody);
+                }
+              });
+            });
+          } else {
+            cb(undefined, respBody);
+          }
+        }
+      });
+};
+
+
+exports.pubkeysForEmail = function(email, cb) {
+  client.query(
+    'SELECT content FROM pubkey WHERE email = (SELECT id FROM email WHERE address = ?)',
+    [ email ],
+    function (err, rows) {
+      var ar = [ ];
+      if (!err) rows.forEach(function(r) { ar.push(r.content); });
+      else logUnexpectedError(err);
+      cb(ar);
+    });
+}
+
+exports.removeEmail = function(authenticated_email, email, cb) {
+  exports.emailsBelongToSameAccount(authenticated_email, email, function(ok) {
+    if (!ok) {
+      logger.log('security', authenticated_email + ' attempted to delete an email that doesn\'t belong to her: ' + email);
+      cb("authenticated user doesn't have permission to remove specified email " + email);
+      return;
+    }
+
+    client.query(
+      'DELETE FROM pubkey WHERE email = ( SELECT id FROM email WHERE address = ? )',
+      [ email ],
+      function (err, info) {
+        if (err) {
+          logUnexpectedError(err);
+          cb(err);
+        } else {
+          client.query(
+            'DELETE FROM email WHERE address = ?',
+            [ email ],
+            function(err, info) {
+              if (err) logUnexpectedError(err);
+              // smash null into undefined
+              cb(err ? err : undefined);
+            });
+        }
+      });
+  });
+}
+
+exports.cancelAccount = function(email, cb) {
+  function reportErr(err) { if (err) logUnexpectedError(err); }
+  client.query(
+    "SELECT user FROM email WHERE address = ?", [ email ],
+    function (err, rows) {
+      if (err) {
+        logUnexpectedError(err)
+        cb(err);
+        return
+      }
+      var uid = rows[0].user;
+      client.query("DELETE LOW_PRIORITY FROM pubkey WHERE email in ( SELECT id FROM email WHERE user = ? )", [ uid ], reportErr);
+      client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], reportErr);
+      client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], reportErr);
+      cb();
+    });
+}
diff --git a/browserid/lib/secrets.js b/browserid/lib/secrets.js
index 7aca40b986e7e82366fd531e22421f17f57b7dff..8018edfbc725f9a1f552f388bab4dcb77e611b1e 100644
--- a/browserid/lib/secrets.js
+++ b/browserid/lib/secrets.js
@@ -2,10 +2,10 @@ const
 path = require('path'),
 fs = require('fs');
 
-function generateSecret() {
+exports.generate = function(chars) {
   var str = "";
   const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-  for (var i=0; i < 128; i++) {
+  for (var i=0; i < chars; i++) {
     str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
   }
   return str;
@@ -19,7 +19,7 @@ exports.hydrateSecret = function(name, dir) {
   try{ secret = fs.readFileSync(p).toString(); } catch(e) {};
 
   if (secret === undefined) {
-    secret = generateSecret();
+    secret = exports.generate(128);
     fs.writeFileSync(p, secret);
   }
   return secret;
diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js
index e7d757956aa8ddae61e51ab6f7ccc4230ce0d0be..d01b952b78b2dd4311b35065fe13bb9d3290e657 100644
--- a/browserid/lib/wsapi.js
+++ b/browserid/lib/wsapi.js
@@ -72,25 +72,26 @@ function setup(app) {
     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(stageParams);
-
-      // store the email being registered in the session data
-      if (!req.session) req.session = {};
+      db.stageUser(stageParams, function(secret) {
+        // 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: stageParams.email,
-        hash: stageParams.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.
-      };
+        // store inside the session the details of this pending verification
+        req.session.pendingVerification = {
+          email: stageParams.email,
+          hash: stageParams.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.
+        };
 
-      resp.json(true);
-      email.sendVerificationEmail(stageParams.email, stageParams.site, secret);
+        resp.json(true);
 
+        // let's now kick out a verification email!
+        email.sendVerificationEmail(stageParams.email, stageParams.site, secret);
+      });
     } catch(e) {
       // we should differentiate tween' 400 and 500 here.
       httputils.badRequest(resp, e.toString());
@@ -128,7 +129,6 @@ function setup(app) {
     } 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.checkAuth(v.email, function(hash) {
         if (hash === v.hash) {
@@ -161,17 +161,17 @@ function setup(app) {
 
   app.post('/wsapi/add_email', checkAuthed, checkParams(["email", "pubkey", "site"]), function (req, resp) {
     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, req.body.email, req.body.pubkey);
+      // on failure stageEmail may throw
+      db.stageEmail(req.session.authenticatedUser, req.body.email, req.body.pubkey, function(secret) {
 
-      // store the email being added in session data
-      req.session.pendingAddition = req.body.email;
+        // store the email being added in session data
+        req.session.pendingAddition = req.body.email;
 
-      resp.json(true);
+        resp.json(true);
 
-      // let's now kick out a verification email!
-      email.sendVerificationEmail(req.body.email, req.body.site, secret);
+        // let's now kick out a verification email!
+        email.sendVerificationEmail(req.body.email, req.body.site, secret);
+      });
     } catch(e) {
       // we should differentiate tween' 400 and 500 here.
       httputils.badRequest(resp, e.toString());
diff --git a/browserid/tests/db-test.js b/browserid/tests/db-test.js
index bd7afd9145bf30bb15b9d2d0f9c6a645b1c353be..f83bde7a6a75f63074dbd573be523aa6668cbff6 100755
--- a/browserid/tests/db-test.js
+++ b/browserid/tests/db-test.js
@@ -12,364 +12,425 @@ var suite = vows.describe('db');
 // disable vows (often flakey?) async error behavior
 suite.options.error = false;
 
-db.dbPath = temp.path({suffix: '.sqlite'});
+function addTestsForDriver(driver) {
+  var dbPath = temp.path({suffix: '.db'});
 
-suite.addBatch({
-  "waiting for the database to become ready": {
-    topic: function() {
-      var cb = this.callback;
-      db.onReady(function() { cb(true) });
-    },
-    "the database is ready": function(r) {
-      assert.strictEqual(r, true);
-    }
-  }
-});
-
-// caching of secrets between test batches.
-var secret = undefined;
-
-suite.addBatch({
-  "an email address is not reported as staged before it is": {
-    topic: function() {
-      return db.isStaged('lloyd@nowhe.re');
-    },
-    "isStaged returns false": function (r) {
-      assert.strictEqual(r, false);
-    }
-  },
-  "an email address is not reported as known before it is": {
-    topic: function() {
-      db.emailKnown('lloyd@nowhe.re', this.callback);
-    },
-    "emailKnown returns false": function (r) {
-      assert.strictEqual(r, false);
-    }
-  }
-});
-
-suite.addBatch({
-  "stage a user for creation pending verification": {
-    topic: function() {
-      return secret = db.stageUser({
-        email: 'lloyd@nowhe.re',
-        pubkey: 'fakepubkey',
-        hash: 'fakepasswordhash'
-      });
-    },
-    "staging returns a valid secret": function(r) {
-      assert.isString(secret);
-      assert.strictEqual(secret.length, 48);
-    }
-  }
-});
-
-suite.addBatch({
-  "an email address is reported": {
-    topic: function() {
-      return db.isStaged('lloyd@nowhe.re');
-    },
-    " as staged after it is": function (r) {
-      assert.strictEqual(r, true);
-    }
-  },
-  "an email address is not reported": {
-    topic: function() {
-      db.emailKnown('lloyd@nowhe.re', this.callback);
-    },
-    " as known when it is only staged": function (r) {
-      assert.strictEqual(r, false);
-    }
-  }
-});
-
-suite.addBatch({
-  "upon receipt of a secret": {
-    topic: function() {
-      db.gotVerificationSecret(secret, this.callback);
-    },
-    "gotVerificationSecret completes without error": function (r) {
-      assert.strictEqual(r, undefined);
-    }
+  if (driver === 'mysql') {
+    // let's check to see if we can connect and render a nice
+    // error message if not.  For community members making casual
+    // contributions, we should expect that they might not want to
+    // set up mysql.
+    suite.addBatch({
+      "mysql server": {
+        topic: function() { db.open({driver: driver, unit_test: true}, this.callback) },
+        "accepting connections": function(err) {
+          if (err) {
+            console.log("MYSQL TESTS WILL FAIL cause cannot connect to a local mysql database (" + err.message + ")");
+          }
+        },
+        "connection closes": {
+          topic: function() { db.close(this.callback); },
+          "without error": function(err) {
+            assert.isUndefined(err);
+          }
+        }
+      }
+    });
   }
-});
 
-suite.addBatch({
-  "an email address is not reported": {
-    topic: function() {
-      return db.isStaged('lloyd@nowhe.re');
+  suite.addBatch({
+    "onReady": {
+      topic: function() { db.onReady(this.callback); },
+      "works": function(r) { }
     },
-    "as staged immediately after its verified": function (r) {
-      assert.strictEqual(r, false);
-    }
-  },
-  "an email address is known": {
-    topic: function() {
-      db.emailKnown('lloyd@nowhe.re', this.callback);
+    "onReady still": {
+      topic: function() { db.onReady(this.callback); },
+      "works for more than one caller": function(r) { }
     },
-    "when it is": function (r) {
-      assert.strictEqual(r, true);
+    "opening the database": {
+      topic: function() {
+        db.open({ driver: driver, unit_test: true, path: dbPath }, this.callback);
+      },
+      "and its ready": function(r) {
+        assert.isUndefined(r);
+      },
+      "doesn't prevent onReady": {
+        topic: function() { db.onReady(this.callback); },
+        "from working": function(r) { }
+      }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "adding keys to email": {
-    topic: function() {
-      db.addKeyToEmail('lloyd@nowhe.re', 'lloyd@nowhe.re', 'fakepubkey2', this.callback);
-    },
-    "works": function(r) {
-      assert.isUndefined(r);
-    }
-  }
-});
+  // caching of secrets between test batches.
+  var secret = undefined;
 
-suite.addBatch({
-  "adding multiple keys to email": {
-    topic: function() {
-      db.addKeyToEmail('lloyd@nowhe.re', 'lloyd@nowhe.re', 'fakepubkey3', this.callback);
+  suite.addBatch({
+    "an email address is not reported as staged before it is": {
+      topic: function() {
+        db.isStaged('lloyd@nowhe.re', this.callback);
+      },
+      "isStaged returns false": function (r) {
+        assert.isFalse(r);
+      }
     },
-    "works too": function(r) {
-      assert.isUndefined(r);
+    "an email address is not reported as known before it is": {
+      topic: function() {
+        db.emailKnown('lloyd@nowhe.re', this.callback);
+      },
+      "emailKnown returns false": function (r) {
+        assert.isFalse(r);
+      }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "pubkeysForEmail": {
-    topic: function() {
-      db.pubkeysForEmail('lloyd@nowhe.re', this.callback);
-    },
-    "returns all public keys properly": function(r) {
-      assert.isArray(r);
-      assert.strictEqual(r.length, 3);
+  suite.addBatch({
+    "stage a user for creation pending verification": {
+      topic: function() {
+        db.stageUser({
+          email: 'lloyd@nowhe.re',
+          pubkey: 'fakepubkey',
+          hash: 'fakepasswordhash'
+        }, this.callback);
+      },
+      "staging returns a valid secret": function(r) {
+        secret = r;
+        assert.isString(secret);
+        assert.strictEqual(secret.length, 48);
+      }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "checkAuth returns": {
-    topic: function() {
-      db.checkAuth('lloyd@nowhe.re', this.callback);
+  suite.addBatch({
+    "an email address is reported": {
+      topic: function() {
+        db.isStaged('lloyd@nowhe.re', this.callback);
+      },
+      " as staged after it is": function (r) {
+        assert.strictEqual(r, true);
+      }
     },
-    "the correct password": function(r) {
-      assert.strictEqual(r, "fakepasswordhash");
+    "an email address is not reported": {
+      topic: function() {
+        db.emailKnown('lloyd@nowhe.re', this.callback);
+      },
+      " as known when it is only staged": function (r) {
+        assert.strictEqual(r, false);
+      }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "staging an email": {
-    topic: function() {
-      return db.stageEmail('lloyd@nowhe.re', 'lloyd@somewhe.re', 'fakepubkey4');
-    },
-    "yields a valid secret": function(secret) {
-      assert.isString(secret);
-      assert.strictEqual(secret.length, 48);
-    },
-    "makes email addr via isStaged": {
-      topic: function() { return db.isStaged('lloyd@somewhe.re'); },
-      "visible": function(r) { assert.isTrue(r); }
-    },
-    "and verifying it": {
-      topic: function(secret) {
+  suite.addBatch({
+    "upon receipt of a secret": {
+      topic: function() {
         db.gotVerificationSecret(secret, this.callback);
       },
-      "returns no error": function(r) {
-        assert.isUndefined(r);
-      },
-      "makes email addr via knownEmail": {
-        topic: function() { db.emailKnown('lloyd@somewhe.re', this.callback); },
-        "visible": function(r) { assert.isTrue(r); }
-      },
-      "makes email addr via isStaged": {
-        topic: function() { return db.isStaged('lloyd@somewhe.re'); },
-        "not visible": function(r) { assert.isFalse(r); }
+      "gotVerificationSecret completes without error": function (r) {
+        assert.strictEqual(r, undefined);
       }
     }
-  }
-});
+  });
 
-// exports.emailsBelongToSameAccount
-suite.addBatch({
-  "emails do belong to the same account": {
-    "is true": { 
+  suite.addBatch({
+    "an email address is not reported": {
       topic: function() {
-        db.emailsBelongToSameAccount('lloyd@nowhe.re', 'lloyd@somewhe.re', this.callback);
+        db.isStaged('lloyd@nowhe.re', this.callback);
       },
-      "when they do": function(r) {
-        assert.isTrue(r);
+      "as staged immediately after its verified": function (r) {
+        assert.strictEqual(r, false);
       }
     },
-    "is false": { 
+    "an email address is known": {
       topic: function() {
-        db.emailsBelongToSameAccount('lloyd@anywhe.re', 'lloyd@somewhe.re', this.callback);
+        db.emailKnown('lloyd@nowhe.re', this.callback);
       },
-      "when they don't": function(r) {
-        assert.isFalse(r);
+      "when it is": function (r) {
+        assert.strictEqual(r, true);
       }
     }
-  }
-});
+  });
 
-// exports.getSyncResponse
-suite.addBatch({
-  "sync responses": {  
-    "are empty": {
+  suite.addBatch({
+    "adding keys to email": {
       topic: function() {
-        db.getSyncResponse('lloyd@nowhe.re',
-                           {
-                             'lloyd@nowhe.re': 'fakepubkey',
-                             'lloyd@somewhe.re': 'fakepubkey4'
-                           },
-                           this.callback);
+        db.addKeyToEmail('lloyd@nowhe.re', 'lloyd@nowhe.re', 'fakepubkey2', this.callback);
       },
-      "when everything is in sync": function (err, resp) {
-        assert.isUndefined(err);
-        assert.isArray(resp.unknown_emails);
-        assert.isArray(resp.key_refresh);
-        assert.strictEqual(resp.unknown_emails.length, 0);
-        assert.strictEqual(resp.key_refresh.length, 0);
+      "works": function(r) {
+        assert.isUndefined(r);
       }
-    },
-    "handles client unknown emails": {
+    }
+  });
+
+  suite.addBatch({
+    "adding multiple keys to email": {
       topic: function() {
-        db.getSyncResponse('lloyd@nowhe.re',
-                           {
-                             'lloyd@nowhe.re': 'fakepubkey'
-                           },
-                           this.callback);
+        db.addKeyToEmail('lloyd@nowhe.re', 'lloyd@nowhe.re', 'fakepubkey3', this.callback);
       },
-      "by returning them in the key_refresh list": function (err, resp) {
-        assert.isUndefined(err);
-        assert.isArray(resp.unknown_emails);
-        assert.isArray(resp.key_refresh);
-        assert.strictEqual(resp.unknown_emails.length, 0);
-        assert.strictEqual(resp.key_refresh.length, 1);
-        assert.strictEqual(resp.key_refresh[0], 'lloyd@somewhe.re');
+      "works too": function(r) {
+        assert.isUndefined(r);
       }
-    },
-    "handles server unknown emails": {
+    }
+  });
+
+  suite.addBatch({
+    "pubkeysForEmail": {
       topic: function() {
-        db.getSyncResponse('lloyd@nowhe.re',
-                           {
-                             'lloyd@nowhe.re': 'fakepubkey',
-                             'lloyd@somewhe.re': 'fakepubkey4',
-                             'lloyd@anywhe.re': 'nofakepubkey',
-                           },
-                           this.callback);
+        db.pubkeysForEmail('lloyd@nowhe.re', this.callback);
       },
-      "by returning them in the unknown_emails list": function (err, resp) {
-        assert.isUndefined(err);
-        assert.isArray(resp.unknown_emails);
-        assert.strictEqual(resp.unknown_emails.length, 1);
-        assert.strictEqual(resp.unknown_emails[0], 'lloyd@anywhe.re');
-        assert.isArray(resp.key_refresh);
-        assert.strictEqual(resp.key_refresh.length, 0);
+      "returns all public keys properly": function(r) {
+        assert.isArray(r);
+        assert.strictEqual(r.length, 3);
       }
-    },
-    "handles server unknown keys": {
+    }
+  });
+
+  suite.addBatch({
+    "checkAuth returns": {
       topic: function() {
-        db.getSyncResponse('lloyd@nowhe.re',
-                           {
-                             'lloyd@nowhe.re': 'fakepubkeyINVALID',
-                             'lloyd@somewhe.re': 'fakepubkey4'
-                           },
-                           this.callback);
+        db.checkAuth('lloyd@nowhe.re', this.callback);
       },
-      "by returning them in the key_refresh list": function (err, resp) {
-        assert.isUndefined(err);
-        assert.isArray(resp.unknown_emails);
-        assert.strictEqual(resp.unknown_emails.length, 0);
-        assert.isArray(resp.key_refresh);
-        assert.strictEqual(resp.key_refresh.length, 1);
-        assert.strictEqual(resp.key_refresh[0], 'lloyd@nowhe.re');
+      "the correct password": function(r) {
+        assert.strictEqual(r, "fakepasswordhash");
       }
-    },
-    "handle more than one case at a time": {
+    }
+  });
+
+  suite.addBatch({
+    "staging an email": {
       topic: function() {
-        db.getSyncResponse('lloyd@nowhe.re',
-                           {
-                             'lloyd@somewhe.re': 'fakepubkeyINVALID',
-                             'lloyd@anywhe.re': 'notreally'
-                           },
-                           this.callback);
+        db.stageEmail('lloyd@nowhe.re', 'lloyd@somewhe.re', 'fakepubkey4', this.callback);
       },
-      "when everything is outta sync": function (err, resp) {
-        assert.isUndefined(err);
-        assert.isArray(resp.unknown_emails);
-        assert.strictEqual(resp.unknown_emails.length, 1);
-        assert.strictEqual(resp.unknown_emails[0], 'lloyd@anywhe.re');
+      "yields a valid secret": function(secret) {
+        assert.isString(secret);
+        assert.strictEqual(secret.length, 48);
+      },
+      "then": {
+        topic: function(secret) {
+          var cb = this.callback;
+          db.isStaged('lloyd@somewhe.re', function(r) { cb(secret, r); });
+        },
+        "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); },
+        "and lets you verify it": {
+          topic: function(secret, r) {
+            db.gotVerificationSecret(secret, this.callback);
+          },
+          "successfully": function(r) {
+            assert.isUndefined(r);
+          },
+          "and knownEmail": {
+            topic: function() { db.emailKnown('lloyd@somewhe.re', this.callback); },
+            "returns true": function(r) { assert.isTrue(r); }
+          },
+          "and isStaged": {
+            topic: function() { db.isStaged('lloyd@somewhe.re', this.callback); },
+            "returns false": function(r) { assert.isFalse(r); }
+          }
+        }
+      }
+    }
+  });
 
-        assert.isArray(resp.key_refresh);
-        assert.strictEqual(resp.key_refresh.length, 2);
-        assert.strictEqual(resp.key_refresh[0], 'lloyd@nowhe.re');
-        assert.strictEqual(resp.key_refresh[1], 'lloyd@somewhe.re');
+  // exports.emailsBelongToSameAccount
+  suite.addBatch({
+    "emails do belong to the same account": {
+      "is true": { 
+        topic: function() {
+          db.emailsBelongToSameAccount('lloyd@nowhe.re', 'lloyd@somewhe.re', this.callback);
+        },
+        "when they do": function(r) {
+          assert.isTrue(r);
+        }
+      },
+      "is false": { 
+        topic: function() {
+          db.emailsBelongToSameAccount('lloyd@anywhe.re', 'lloyd@somewhe.re', this.callback);
+        },
+        "when they don't": function(r) {
+          assert.isFalse(r);
+        }
       }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "removing an existing email": {
-    topic: function() {
-      db.removeEmail("lloyd@somewhe.re", "lloyd@nowhe.re", this.callback);
-    },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
-    },
-    "causes emailKnown": {
-      topic: function() {
-        db.emailKnown('lloyd@nowhe.re', this.callback);
+  // exports.getSyncResponse
+  suite.addBatch({
+    "sync responses": {  
+      "are empty": {
+        topic: function() {
+          db.getSyncResponse('lloyd@nowhe.re',
+                             {
+                               'lloyd@nowhe.re': 'fakepubkey',
+                               'lloyd@somewhe.re': 'fakepubkey4'
+                             },
+                             this.callback);
+        },
+        "when everything is in sync": function (err, resp) {
+          assert.isUndefined(err);
+          assert.isArray(resp.unknown_emails);
+          assert.isArray(resp.key_refresh);
+          assert.strictEqual(resp.unknown_emails.length, 0);
+          assert.strictEqual(resp.key_refresh.length, 0);
+        }
       },
-      "to return false": function (r) {
-        assert.strictEqual(r, false);
+      "handles client unknown emails": {
+        topic: function() {
+          db.getSyncResponse('lloyd@nowhe.re',
+                             {
+                               'lloyd@nowhe.re': 'fakepubkey'
+                             },
+                             this.callback);
+        },
+        "by returning them in the key_refresh list": function (err, resp) {
+          assert.isUndefined(err);
+          assert.isArray(resp.unknown_emails);
+          assert.isArray(resp.key_refresh);
+          assert.strictEqual(resp.unknown_emails.length, 0);
+          assert.strictEqual(resp.key_refresh.length, 1);
+          assert.strictEqual(resp.key_refresh[0], 'lloyd@somewhe.re');
+        }
+      },
+      "handles server unknown emails": {
+        topic: function() {
+          db.getSyncResponse('lloyd@nowhe.re',
+                             {
+                               'lloyd@nowhe.re': 'fakepubkey',
+                               'lloyd@somewhe.re': 'fakepubkey4',
+                               'lloyd@anywhe.re': 'nofakepubkey',
+                             },
+                             this.callback);
+        },
+        "by returning them in the unknown_emails list": function (err, resp) {
+          assert.isUndefined(err);
+          assert.isArray(resp.unknown_emails);
+          assert.strictEqual(resp.unknown_emails.length, 1);
+          assert.strictEqual(resp.unknown_emails[0], 'lloyd@anywhe.re');
+          assert.isArray(resp.key_refresh);
+          assert.strictEqual(resp.key_refresh.length, 0);
+        }
+      },
+      "handles server unknown keys": {
+        topic: function() {
+          db.getSyncResponse('lloyd@nowhe.re',
+                             {
+                               'lloyd@nowhe.re': 'fakepubkeyINVALID',
+                               'lloyd@somewhe.re': 'fakepubkey4'
+                             },
+                             this.callback);
+        },
+        "by returning them in the key_refresh list": function (err, resp) {
+          assert.isUndefined(err);
+          assert.isArray(resp.unknown_emails);
+          assert.strictEqual(resp.unknown_emails.length, 0);
+          assert.isArray(resp.key_refresh);
+          assert.strictEqual(resp.key_refresh.length, 1);
+          assert.strictEqual(resp.key_refresh[0], 'lloyd@nowhe.re');
+        }
+      },
+      "handle more than one case at a time": {
+        topic: function() {
+          db.getSyncResponse('lloyd@nowhe.re',
+                             {
+                               'lloyd@somewhe.re': 'fakepubkeyINVALID',
+                               'lloyd@anywhe.re': 'notreally'
+                             },
+                             this.callback);
+        },
+        "when everything is outta sync": function (err, resp) {
+          assert.isUndefined(err);
+          assert.isArray(resp.unknown_emails);
+          assert.strictEqual(resp.unknown_emails.length, 1);
+          assert.strictEqual(resp.unknown_emails[0], 'lloyd@anywhe.re');
+
+          assert.isArray(resp.key_refresh);
+          assert.strictEqual(resp.key_refresh.length, 2);
+          assert.strictEqual(resp.key_refresh[0], 'lloyd@nowhe.re');
+          assert.strictEqual(resp.key_refresh[1], 'lloyd@somewhe.re');
+        }
       }
     }
-  }
-});
+  });
 
-suite.addBatch({
-  "canceling an account": {
-    topic: function() {
-      db.cancelAccount("lloyd@somewhe.re", this.callback);
-    },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
-    },
-    "causes emailKnown": {
+  suite.addBatch({
+    "removing an existing email": {
       topic: function() {
-        db.emailKnown('lloyd@somewhe.re', this.callback);
+        db.removeEmail("lloyd@somewhe.re", "lloyd@nowhe.re", this.callback);
       },
-      "to return false": function (r) {
-        assert.strictEqual(r, false);
+      "returns no error": function(r) {
+        assert.isUndefined(r);
+      },
+      "causes emailKnown": {
+        topic: function() {
+          db.emailKnown('lloyd@nowhe.re', this.callback);
+        },
+        "to return false": function (r) {
+          assert.strictEqual(r, false);
+        }
       }
     }
-  }
-});
+  });
 
-// exports.cancelAccount
-// exports.removeEmail
+  suite.addBatch({
+    "canceling an account": {
+      topic: function() {
+        db.cancelAccount("lloyd@somewhe.re", this.callback);
+      },
+      "returns no error": function(r) {
+        assert.isUndefined(r);
+      },
+      "causes emailKnown": {
+        topic: function() {
+          db.emailKnown('lloyd@somewhe.re', this.callback);
+        },
+        "to return false": function (r) {
+          assert.strictEqual(r, false);
+        }
+      }
+    }
+  });
 
-suite.addBatch({
-  "remove the database file": {
-    topic: function() {
-      fs.unlink(db.dbPath, this.callback);
-    },
-    "and unlink should not error": function(err) {
-      assert.isNull(err);
-    },
-    "and the file": {
+  suite.addBatch({
+    "closing the database": {
       topic: function() {
-        path.exists(db.dbPath, this.callback);
+        db.close(this.callback);
       },
-      "should be missing": function(r) {
-        assert.isFalse(r);
+      "should work": function(err) {
+        assert.isUndefined(err);
       }
     }
+  });
+
+  if (driver !== 'mysql') {
+    suite.addBatch({
+      "remove the database file": {
+        topic: function() {
+          fs.unlink(dbPath, this.callback);
+        },
+        "and unlink should not error": function(err) {
+          assert.isNull(err);
+        },
+        "and the file": {
+          topic: function() {
+            path.exists(dbPath, this.callback);
+          },
+          "should be missing": function(r) {
+            assert.isFalse(r);
+          }
+        }
+      }
+    });
+  }
+}
+
+// test all available drivers
+files = fs.readdirSync(path.join(__dirname, "..", "lib"));
+
+files.forEach(function(f) {
+  var m = /^db_(.+)\.js$/.exec(f);
+  if (m) {
+    addTestsForDriver(m[1]);
   }
 });
 
 // run or export the suite.
 if (process.argv[1] === __filename) suite.run();
 else suite.export(module);
+
diff --git a/browserid/tests/forgotten-email-test.js b/browserid/tests/forgotten-email-test.js
index 1a7d015476b3ec4076256c9fe4f3ace339a597f5..2fe7cffc5a8dcde8d685f3260a5cfc45192c134c 100755
--- a/browserid/tests/forgotten-email-test.js
+++ b/browserid/tests/forgotten-email-test.js
@@ -1,13 +1,16 @@
 #!/usr/bin/env node
 
 const assert = require('assert'),
-      vows = require('vows'),
-      start_stop = require('./lib/start-stop.js'),
-      wsapi = require('./lib/wsapi.js'),
-      interceptor = require('./lib/email-interceptor.js');
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+interceptor = require('./lib/email-interceptor.js');
 
 var suite = vows.describe('forgotten-email');
 
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
 start_stop.addStartupBatches(suite);
 
 // ever time a new token is sent out, let's update the global
diff --git a/libs/configuration.js b/libs/configuration.js
index 2690dc1f2fa1eb02b72e072f1f0cca4e436cd439..4ad59da7fe5963ac8b5d9489e910e3b5378c7ff8 100644
--- a/libs/configuration.js
+++ b/libs/configuration.js
@@ -26,28 +26,32 @@ const g_configs = {
     port: '443',
     scheme: 'https',
     use_minified_resources: true,
-    log_path: '/home/browserid/var/'
+    log_path: '/home/browserid/var/',
+    database: { driver: "mysql" }
   },
   development: {
     hostname: 'dev.diresworb.org',
     port: '443',
     scheme: 'https',
     use_minified_resources: true,
-    log_path: '/home/browserid/var/'
+    log_path: '/home/browserid/var/',
+    database: { driver: "mysql" }
   },
   beta: {
     hostname: 'diresworb.org',
     port: '443',
     scheme: 'https',
     use_minified_resources: true,
-    log_path: '/home/browserid/var/'
+    log_path: '/home/browserid/var/',
+    database: { driver: "mysql" }
   },
   local: {
     hostname: '127.0.0.1',
     port: '10002',
     scheme: 'http',
     use_minified_resources: false,
-    log_path: './'
+    log_path: './',
+    database: { driver: "json" }
   }
 };
 
diff --git a/libs/logging.js b/libs/logging.js
index c34aaa0c15ad10cd60c1f1c93b3f7968fb02dc0e..1d18ef520ea635ec36a57ed302728a543e37a6a7 100644
--- a/libs/logging.js
+++ b/libs/logging.js
@@ -35,7 +35,7 @@ exports.log = function(category, entry) {
 
   // timestamp
   entry.at = new Date().toUTCString();
-  
+
   // if no logger, go to console (FIXME: do we really want to log to console?)
   LOGGERS[category].info(JSON.stringify(entry));
 };
diff --git a/package.json b/package.json
index 2b803d4f04ccf5e29ec72e3e131872acbfbbab18..4691170022a0f6cfdb923871eabe62af751055ca 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,6 @@
   , "dependencies": {
       "express": "2.4.3"
     , "xml2js": "0.1.5"
-    , "sqlite": "1.0.3"
     , "nodemailer": "0.1.18"
     , "mustache": "0.3.1-dev"
     , "cookie-sessions": "0.0.2"
@@ -15,7 +14,9 @@
     , "temp": "0.2.0"
     , "express-csrf": "0.3.2"
     , "uglify-js": "1.0.6"
+    , "JSONSelect": "0.2.1"
     , "winston" : "0.3.3"
-    , "connect-cookie-session" : "0.0.1"    
+    , "connect-cookie-session" : "0.0.1"
+    , "mysql" : "0.9.1"
   }
-}
\ No newline at end of file
+}
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a0ef1563a8e8f5556edc3d85fce2c29c7936278f
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+VOWS=`which vows 2> /dev/null`
+if [ ! -x "$VOWS" ]; then
+    echo "vows not found in your path.  try:  npm install -g vows"
+    exit 1
+fi
+
+for file in browserid/tests/*.js ; do
+    vows $file
+    if [[ $? != 0 ]] ; then
+        exit 1
+    fi
+done