Repository: rickbergfalk/sqlpad
Pull request #540 (2020-02-19 23:39:41)

Refactor: pass nedb into app and models
Refactors app and models to take nedb as a parameter in an effort to reduce references to config and db/models objects, which is an effort to help make things easier to test (#539)

Some notes on how things are arranged with this PR:

`/lib/db.js` manages creating and getting the nedb instance. Getting the nedb via `getNedb` should be done as little as possible, as it is kind of an escape hatch to deal with cases where nedb isn't being passed in to various functions. For most cases, the nedb instance should be pulled from `req.nedb`. 

All model utilities have been restructured to be the result of a factory function of sorts, taking an nedb instance to build. These model utilities have been modified to be stateless functions. 

A common pattern throughout the application is to take the `nedb` reference from `req` and build model utilities from it.

This gets a bit repetitive, but enables auto complete in vs code, and provides some clarity as to where these functions are coming from. (If there's a way to note this in the JS using some fancy jsdoc syntax to provide this benefit, I'd be open to populating `req.models` in middleware and removing this repetitive code.)


Patch:
diff --git a/server/app.js b/server/app.js
index 1e206c7f..a78c59b4 100644
--- a/server/app.js
+++ b/server/app.js
@@ -10,10 +10,13 @@ const logger = require('./lib/logger');
  * Create an express app using config
  * @param {object} config
  */
-function makeApp(config) {
+function makeApp(config, nedb) {
   if (typeof config.get !== 'function') {
     throw new Error('config is required to create app');
   }
+  if (!nedb) {
+    throw new Error('nedb is required to create app');
+  }
 
   const baseUrl = config.get('baseUrl');
   const dbPath = config.get('dbPath');
@@ -57,9 +60,10 @@ function makeApp(config) {
   app.use(helmet.xssFilter());
   app.use(helmet.referrerPolicy({ policy: 'same-origin' }));
 
-  // Add config to req.config
+  // Decorate req with app things
   app.use(function(req, res, next) {
     req.config = config;
+    req.nedb = nedb;
     next();
   });
 
diff --git a/server/lib/connectionsFromConfig.js b/server/lib/connectionsFromConfig.js
new file mode 100644
index 00000000..44b7fdd6
--- /dev/null
+++ b/server/lib/connectionsFromConfig.js
@@ -0,0 +1,79 @@
+const _ = require('lodash');
+const logger = require('./logger');
+const drivers = require('../drivers');
+const getConfigFromFile = require('./config/fromFile.js');
+const [configFromFile] = getConfigFromFile() || {};
+
+/**
+ * Get connections from config.
+ *
+ * For environment variables:
+ * connection env vars must follow the format:
+ * SQLPAD_CONNECTIONS__<connectionId>__<connectionFieldName>
+ *
+ * <connectionId> can be any value to associate a grouping a fields to a connection instance
+ * If supplying a connection that was previously defined in the nedb database,
+ * this would map internally to connection._id object.
+ *
+ * <connectionFieldName> should be a field name identified in drivers.
+ *
+ * To define connections via envvars, `driver` field should be supplied.
+ * _id field is not required, as it is defined in second env var fragment.
+ *
+ * Example: SQLPAD_CONNECTIONS__ab123__sqlserverEncrypt=""
+ *
+ * From file, resulting parsed configuration from file is expected to follow format `connections.<id>.<fieldname>`
+ * {
+ *   connections: {
+ *     ab123: {
+ *       sqlserverEncrypt: true
+ *     }
+ *   }
+ * }
+ *
+ * @param {object} env
+ * @returns {array<object>} arrayOfConnections
+ */
+function getConnectionsFromConfig(env = process.env) {
+  // Create a map of connections from parsing environment variable
+  const connectionsMapFromEnv = Object.keys(env)
+    .filter(key => key.startsWith('SQLPAD_CONNECTIONS__'))
+    .reduce((connectionsMap, envVar) => {
+      // eslint-disable-next-line no-unused-vars
+      const [prefix, id, field] = envVar.split('__');
+      if (!connectionsMap[id]) {
+        connectionsMap[id] = {};
+      }
+      connectionsMap[id][field] = env[envVar];
+      return connectionsMap;
+    }, {});
+
+  // Get copy of connections from config file
+  const { connections } = _.cloneDeep(configFromFile);
+
+  // connections key from file matches format that is constructed from env
+  // merge the 2 together then create an array out of them
+  const connectionsMap = { ...connectionsMapFromEnv, ...connections };
+
+  const connectionsFromConfig = [];
+  Object.keys(connectionsMap).forEach(id => {
+    try {
+      let connection = connectionsMap[id];
+      connection._id = id;
+      connection = drivers.validateConnection(connection);
+      connection.editable = false;
+      connectionsFromConfig.push(connection);
+    } catch (error) {
+      logger.error(
+        error,
+        'Environment connection configuration failed for %s',
+        id
+      );
+    }
+  });
+  return connectionsFromConfig;
+}
+
+module.exports = {
+  getConnectionsFromConfig
+};
diff --git a/server/lib/db.js b/server/lib/db.js
index e4f07b98..76984fb1 100644
--- a/server/lib/db.js
+++ b/server/lib/db.js
@@ -2,52 +2,82 @@ const path = require('path');
 const datastore = require('nedb-promise');
 const mkdirp = require('mkdirp');
 const logger = require('./logger');
-const config = require('./config');
+const ensureAdmin = require('./ensureAdmin');
 const consts = require('./consts');
-const passhash = require('../lib/passhash');
+const getModels = require('../models');
 
-const admin = config.get('admin');
-const adminPassword = config.get('adminPassword');
-const dbPath = config.get('dbPath');
-const allowConnectionAccessToEveryone = config.get(
-  'allowConnectionAccessToEveryone'
-);
+const TEN_MINUTES = 1000 * 60 * 10;
+const FIVE_MINUTES = 1000 * 60 * 5;
 
-mkdirp.sync(path.join(dbPath, '/cache'));
+/**
+ * Whenever possible nedb should be read from the app req object.
+ * Sometimes this isn't possible or convenient at the moment however.
+ * For those cases, this module provides access by caching the nedb instance.
+ * Safety measures have been added to ensure only 1 instance of nedb can be initialized per alias
+ */
+let instances = {};
 
-const db = {
-  users: datastore({ filename: path.join(dbPath, 'users.db') }),
-  connections: datastore({
-    filename: path.join(dbPath, 'connections.db')
-  }),
-  connectionAccesses: datastore({
-    filename: path.join(dbPath, 'connectionaccesses.db')
-  }),
-  queries: datastore({ filename: path.join(dbPath, 'queries.db') }),
-  queryHistory: datastore({ filename: path.join(dbPath, 'queryhistory.db') }),
-  cache: datastore({ filename: path.join(dbPath, 'cache.db') }),
-  instances: [
-    'users',
-    'connections',
-    'connectionAccesses',
-    'queries',
-    'queryHistory',
-    'cache'
-  ]
-};
+/**
+ * Get nedb instance for an optional alias.
+ * Returns promise of nedb instance
+ * @param {string} [instanceAlias]
+ */
+function getNedb(instanceAlias = 'default') {
+  const nedb = instances[instanceAlias];
+  if (!nedb) {
+    throw new Error('nedb instance must be created first');
+  }
+  // nedb will already be a promise -- this just makes it explicit
+  return Promise.resolve(nedb);
+}
+
+/**
+ * Initialize nedb for a given config
+ * @param {object} config
+ */
+async function initNedb(config) {
+  const admin = config.get('admin');
+  const adminPassword = config.get('adminPassword');
+  const dbPath = config.get('dbPath');
+  const allowConnectionAccessToEveryone = config.get(
+    'allowConnectionAccessToEveryone'
+  );
+
+  mkdirp.sync(path.join(dbPath, '/cache'));
 
-// Load dbs, migrate data, and apply indexes
-async function init() {
+  const nedb = {
+    users: datastore({ filename: path.join(dbPath, 'users.db') }),
+    connections: datastore({
+      filename: path.join(dbPath, 'connections.db')
+    }),
+    connectionAccesses: datastore({
+      filename: path.join(dbPath, 'connectionaccesses.db')
+    }),
+    queries: datastore({ filename: path.join(dbPath, 'queries.db') }),
+    queryHistory: datastore({ filename: path.join(dbPath, 'queryhistory.db') }),
+    cache: datastore({ filename: path.join(dbPath, 'cache.db') }),
+    instances: [
+      'users',
+      'connections',
+      'connectionAccesses',
+      'queries',
+      'queryHistory',
+      'cache'
+    ]
+  };
+
+  // Load dbs, migrate data, and apply indexes
   await Promise.all(
-    db.instances.map(dbname => {
+    nedb.instances.map(dbname => {
       logger.info('Loading %s', dbname);
-      return db[dbname].loadDatabase();
+      return nedb[dbname].loadDatabase();
     })
   );
+
   // create default connection accesses
   if (allowConnectionAccessToEveryone) {
     logger.info('Creating access on every connection to every user...');
-    await db.connectionAccesses.update(
+    await nedb.connectionAccesses.update(
       {
         connectionId: consts.EVERY_CONNECTION_ID,
         userId: consts.EVERYONE_ID
@@ -65,60 +95,52 @@ async function init() {
       }
     );
   }
+
   // Apply indexes
-  await db.users.ensureIndex({ fieldName: 'email', unique: true });
-  await db.cache.ensureIndex({ fieldName: 'cacheKey', unique: true });
-  await db.connectionAccesses.ensureIndex({ fieldName: 'connectionId' });
-  await db.connectionAccesses.ensureIndex({ fieldName: 'userId' });
-  await db.queryHistory.ensureIndex({ fieldName: 'connectionName' });
-  await db.queryHistory.ensureIndex({ fieldName: 'userEmail' });
-  await db.queryHistory.ensureIndex({ fieldName: 'createdDate' });
-  // set autocompaction
-  const tenMinutes = 1000 * 60 * 10;
-  db.instances.forEach(dbname => {
-    db[dbname].nedb.persistence.setAutocompactionInterval(tenMinutes);
+  await nedb.users.ensureIndex({ fieldName: 'email', unique: true });
+  await nedb.cache.ensureIndex({ fieldName: 'cacheKey', unique: true });
+  await nedb.connectionAccesses.ensureIndex({ fieldName: 'connectionId' });
+  await nedb.connectionAccesses.ensureIndex({ fieldName: 'userId' });
+  await nedb.queryHistory.ensureIndex({ fieldName: 'connectionName' });
+  await nedb.queryHistory.ensureIndex({ fieldName: 'userEmail' });
+  await nedb.queryHistory.ensureIndex({ fieldName: 'createdDate' });
+
+  // Set autocompaction
+  nedb.instances.forEach(dbname => {
+    nedb[dbname].nedb.persistence.setAutocompactionInterval(TEN_MINUTES);
   });
-  return ensureAdmin();
-}
 
-db.loadPromise = init();
+  // Schedule cleanups
+  const models = getModels(nedb);
+  setInterval(async () => {
+    await models.resultCache.removeExpired();
+    await models.queryHistory.removeOldEntries();
+  }, FIVE_MINUTES);
 
-async function ensureAdmin() {
-  const adminEmail = admin;
-  if (!adminEmail) {
-    return;
-  }
+  // Ensure admin is set as specified if provided
+  await ensureAdmin(nedb, admin, adminPassword);
 
-  try {
-    // if an admin was passed in the command line, check to see if a user exists with that email
-    // if so, set the admin to true
-    // if not, whitelist the email address.
-    // Then log that the person should visit the signup url to finish registration.
-    const user = await db.users.findOne({ email: adminEmail });
-    if (user) {
-      const changes = { role: 'admin' };
-      if (adminPassword) {
-        changes.passhash = await passhash.getPasshash(adminPassword);
-      }
-      await db.users.update({ _id: user._id }, { $set: changes }, {});
-      logger.info('Admin access granted to %s', adminEmail);
-      return;
-    }
+  return nedb;
+}
 
-    const newAdmin = {
-      email: adminEmail,
-      role: 'admin'
-    };
-    if (adminPassword) {
-      newAdmin.passhash = await passhash.getPasshash(adminPassword);
-    }
-    await db.users.insert(newAdmin);
-    logger.info('Admin access granted to %s', adminEmail);
-    logger.info('Please visit signup to complete registration.');
-  } catch (error) {
-    logger.error('Admin access grant failed for %s', adminEmail);
-    throw error;
+/**
+ * Initializes an nedb instance for a given config.
+ * Ensures that this only happens once for a given alias.
+ * If this were called multiple times weird things could happen
+ * @param {object} config
+ * @param {string} instanceAlias
+ */
+function makeNedb(config, instanceAlias = 'default') {
+  // makeNedb should only be called once for a given alias
+  if (instances[instanceAlias]) {
+    throw new Error(`db instance ${instanceAlias} already made`);
   }
+  const dbPromise = initNedb(config);
+  instances[instanceAlias] = dbPromise;
+  return dbPromise;
 }
 
-module.exports = db;
+module.exports = {
+  makeNedb,
+  getNedb
+};
diff --git a/server/lib/ensureAdmin.js b/server/lib/ensureAdmin.js
new file mode 100644
index 00000000..a386700d
--- /dev/null
+++ b/server/lib/ensureAdmin.js
@@ -0,0 +1,48 @@
+const passhash = require('./passhash');
+const logger = require('./logger');
+
+/**
+ * Ensure admin email is a user if provided
+ * If password is provided, update password
+ * @param {object} nedb
+ * @param {string} adminEmail
+ * @param {string} adminPassword
+ */
+async function ensureAdmin(nedb, adminEmail, adminPassword) {
+  if (!adminEmail) {
+    return;
+  }
+
+  try {
+    // if an admin was passed in the command line, check to see if a user exists with that email
+    // if so, set the admin to true
+    // if not, whitelist the email address.
+    // Then log that the person should visit the signup url to finish registration.
+    const user = await nedb.users.findOne({ email: adminEmail });
+    if (user) {
+      const changes = { role: 'admin' };
+      if (adminPassword) {
+        changes.passhash = await passhash.getPasshash(adminPassword);
+      }
+      await nedb.users.update({ _id: user._id }, { $set: changes }, {});
+      logger.info('Admin access granted to %s', adminEmail);
+      return;
+    }
+
+    const newAdmin = {
+      email: adminEmail,
+      role: 'admin'
+    };
+    if (adminPassword) {
+      newAdmin.passhash = await passhash.getPasshash(adminPassword);
+    }
+    await nedb.users.insert(newAdmin);
+    logger.info('Admin access granted to %s', adminEmail);
+    logger.info('Please visit signup to complete registration.');
+  } catch (error) {
+    logger.error('Admin access grant failed for %s', adminEmail);
+    throw error;
+  }
+}
+
+module.exports = ensureAdmin;
diff --git a/server/middleware/must-have-connection-access-or-chart-link-noauth.js b/server/middleware/must-have-connection-access-or-chart-link-noauth.js
index b8ce3b58..853a3a90 100644
--- a/server/middleware/must-have-connection-access-or-chart-link-noauth.js
+++ b/server/middleware/must-have-connection-access-or-chart-link-noauth.js
@@ -1,18 +1,19 @@
 const mustBeAuthenticated = require('./must-be-authenticated');
-const connectionAccessesUtil = require('../models/connectionAccesses.js');
+const getModels = require('../models');
 
 // If admin or has connection access then continue
 // If no access don't continue but return 200 with an error message
 module.exports = [
   mustBeAuthenticated,
   async function mustHaveConnectionAccessOrChartLinkNoAuth(req, res, next) {
+    const models = getModels(req.nedb);
     if (
       req.user.role === 'admin' ||
       !req.config.get('tableChartLinksRequireAuth')
     ) {
       return next();
     }
-    const connectionAccess = await connectionAccessesUtil.findOneActiveByConnectionIdAndUserId(
+    const connectionAccess = await models.connectionAccesses.findOneActiveByConnectionIdAndUserId(
       req.body.connectionId,
       req.user.id
     );
diff --git a/server/middleware/must-have-connection-access.js b/server/middleware/must-have-connection-access.js
index 09105c7b..82459384 100644
--- a/server/middleware/must-have-connection-access.js
+++ b/server/middleware/must-have-connection-access.js
@@ -1,16 +1,17 @@
 const mustBeAuthenticated = require('./must-be-authenticated');
-const connectionAccessesUtil = require('../models/connectionAccesses.js');
+const getModels = require('../models');
 
 // If admin or has connection access then continue
 // If no access don't continue but return 200 with an error message
 module.exports = [
   mustBeAuthenticated,
   async function mustHaveConnectionAccess(req, res, next) {
+    const models = getModels(req.nedb);
     if (req.user.role === 'admin') {
       return next();
     }
     const connectionId = req.params.connectionId || req.body.connectionId;
-    const connectionAccess = await connectionAccessesUtil.findOneActiveByConnectionIdAndUserId(
+    const connectionAccess = await models.connectionAccesses.findOneActiveByConnectionIdAndUserId(
       connectionId,
       req.user.id
     );
diff --git a/server/middleware/passport.js b/server/middleware/passport.js
index 5e5b617f..cdbe5dac 100644
--- a/server/middleware/passport.js
+++ b/server/middleware/passport.js
@@ -1,5 +1,6 @@
 const passport = require('passport');
-const usersUtil = require('../models/users.js');
+const { getNedb } = require('../lib/db');
+const getModels = require('../models');
 
 // For actual passport strategy implementations, refer to related route files:
 //   Local & Basic:  routes/signup-signin-signout.js
@@ -14,7 +15,9 @@ passport.serializeUser(function(user, done) {
 
 passport.deserializeUser(async function(id, done) {
   try {
-    const user = await usersUtil.findOneById(id);
+    const nedb = await getNedb();
+    const models = getModels(nedb);
+    const user = await models.users.findOneById(id);
     if (user) {
       return done(null, {
         id: user._id,
diff --git a/server/models/connectionAccesses.js b/server/models/connectionAccesses.js
index 60d0f60e..cead176a 100644
--- a/server/models/connectionAccesses.js
+++ b/server/models/connectionAccesses.js
@@ -1,5 +1,4 @@
 const Joi = require('@hapi/joi');
-const db = require('../lib/db.js');
 const consts = require('../lib/consts');
 
 const schema = Joi.object({
@@ -18,123 +17,127 @@ const schema = Joi.object({
   modifiedDate: Joi.date().default(Date.now)
 });
 
-async function save(data) {
-  if (!data.connectionId) {
-    throw new Error('connectionId required when saving connection access');
-  }
+function makeConnectionAccesses(nedb) {
+  async function save(data) {
+    if (!data.connectionId) {
+      throw new Error('connectionId required when saving connection access');
+    }
 
-  if (!data.userId) {
-    throw new Error('userId required when saving connection access');
-  }
+    if (!data.userId) {
+      throw new Error('userId required when saving connection access');
+    }
 
-  if (data.duration > 0) {
-    data.expiryDate = new Date(new Date().getTime() + data.duration * 1000);
-  } else {
-    data.expiryDate = new Date().setFullYear(2099);
-  }
+    if (data.duration > 0) {
+      data.expiryDate = new Date(new Date().getTime() + data.duration * 1000);
+    } else {
+      data.expiryDate = new Date().setFullYear(2099);
+    }
+
+    const joiResult = schema.validate(data);
+    if (joiResult.error) {
+      return Promise.reject(joiResult.error);
+    }
 
-  const joiResult = schema.validate(data);
-  if (joiResult.error) {
-    return Promise.reject(joiResult.error);
+    await nedb.connectionAccesses.update(
+      {
+        connectionId: data.connectionId,
+        connectionName: data.connectionName,
+        userId: data.userId,
+        userEmail: data.userEmail,
+        duration: data.duration,
+        expiryDate: data.expiryDate
+      },
+      joiResult.value,
+      {
+        upsert: true
+      }
+    );
+    return findOneActiveByConnectionIdAndUserId(data.connectionId, data.userId);
   }
 
-  await db.connectionAccesses.update(
-    {
-      connectionId: data.connectionId,
-      connectionName: data.connectionName,
-      userId: data.userId,
-      userEmail: data.userEmail,
-      duration: data.duration,
-      expiryDate: data.expiryDate
-    },
-    joiResult.value,
-    {
-      upsert: true
+  async function expire(id) {
+    const connectionAccess = await nedb.connectionAccesses.findOne({ _id: id });
+    if (!connectionAccess) {
+      throw new Error('Connection access not found');
     }
-  );
-  return findOneActiveByConnectionIdAndUserId(data.connectionId, data.userId);
-}
 
-async function expire(id) {
-  const connectionAccess = await db.connectionAccesses.findOne({ _id: id });
-  if (!connectionAccess) {
-    throw new Error('Connection access not found');
-  }
+    connectionAccess.expiryDate = new Date();
+    connectionAccess.modifiedDate = new Date();
 
-  connectionAccess.expiryDate = new Date();
-  connectionAccess.modifiedDate = new Date();
+    await nedb.connectionAccesses.update({ _id: id }, connectionAccess);
+    return findOneById(id);
+  }
 
-  await db.connectionAccesses.update({ _id: id }, connectionAccess);
-  return findOneById(id);
-}
+  function findOneById(id) {
+    return nedb.connectionAccesses.findOne({ _id: id });
+  }
 
-function findOneById(id) {
-  return db.connectionAccesses.findOne({ _id: id });
-}
+  function findOneActiveByConnectionIdAndUserId(connectionId, userId) {
+    return nedb.connectionAccesses.findOne({
+      $and: [
+        {
+          $or: [
+            {
+              connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] },
+              userId
+            },
+            {
+              connectionId,
+              userId: { $in: [connectionId, consts.EVERYONE_ID] }
+            },
+            {
+              connectionId: consts.EVERY_CONNECTION_ID,
+              userId: consts.EVERYONE_ID
+            }
+          ],
+          expiryDate: { $gt: new Date() }
+        }
+      ]
+    });
+  }
 
-function findOneActiveByConnectionIdAndUserId(connectionId, userId) {
-  return db.connectionAccesses.findOne({
-    $and: [
-      {
-        $or: [
-          {
-            connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] },
-            userId
-          },
-          {
-            connectionId,
-            userId: { $in: [connectionId, consts.EVERYONE_ID] }
-          },
-          {
-            connectionId: consts.EVERY_CONNECTION_ID,
-            userId: consts.EVERYONE_ID
-          }
-        ],
-        expiryDate: { $gt: new Date() }
-      }
-    ]
-  });
-}
+  function findAllActiveByConnectionId(connectionId) {
+    return nedb.connectionAccesses.findOne({
+      connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] },
+      expiryDate: { $gt: new Date() }
+    });
+  }
 
-function findAllActiveByConnectionId(connectionId) {
-  return db.connectionAccesses.findOne({
-    connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] },
-    expiryDate: { $gt: new Date() }
-  });
-}
+  function findAllByConnectionId(connectionId) {
+    return nedb.connectionAccesses.findAll({
+      connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] }
+    });
+  }
 
-function findAllByConnectionId(connectionId) {
-  return db.connectionAccesses.findAll({
-    connectionId: { $in: [connectionId, consts.EVERY_CONNECTION_ID] }
-  });
-}
+  function findAllActive() {
+    return nedb.connectionAccesses
+      .cfind({ expiryDate: { $gt: new Date() } })
+      .sort({ expiryDate: -1 })
+      .exec();
+  }
 
-function findAllActive() {
-  return db.connectionAccesses
-    .cfind({ expiryDate: { $gt: new Date() } })
-    .sort({ expiryDate: -1 })
-    .exec();
-}
+  function findAll() {
+    return nedb.connectionAccesses
+      .cfind({}, {})
+      .sort({ expiryDate: -1 })
+      .exec();
+  }
 
-function findAll() {
-  return db.connectionAccesses
-    .cfind({}, {})
-    .sort({ expiryDate: -1 })
-    .exec();
-}
+  function removeById(id) {
+    return nedb.connectionAccesses.remove({ _id: id });
+  }
 
-function removeById(id) {
-  return db.connectionAccesses.remove({ _id: id });
+  return {
+    findOneById,
+    findOneActiveByConnectionIdAndUserId,
+    findAllActiveByConnectionId,
+    findAllByConnectionId,
+    findAllActive,
+    findAll,
+    removeById,
+    expire,
+    save
+  };
 }
 
-module.exports = {
-  findOneById,
-  findOneActiveByConnectionIdAndUserId,
-  findAllActiveByConnectionId,
-  findAllByConnectionId,
-  findAllActive,
-  findAll,
-  removeById,
-  expire,
-  save
-};
+module.exports = makeConnectionAccesses;
diff --git a/server/models/connections.js b/server/models/connections.js
index 7ebb9085..e9098e54 100644
--- a/server/models/connections.js
+++ b/server/models/connections.js
@@ -1,85 +1,12 @@
-const db = require('../lib/db.js');
-const logger = require('../lib/logger');
 const _ = require('lodash');
 const drivers = require('../drivers');
 const makeCipher = require('../lib/makeCipher');
 const config = require('../lib/config');
-const getConfigFromFile = require('../lib/config/fromFile.js');
-
-const [configFromFile] = getConfigFromFile() || {};
+// TODO: during app init upsert these into db?
+const { getConnectionsFromConfig } = require('../lib/connectionsFromConfig');
 
 const { cipher, decipher } = makeCipher(config.get('passphrase'));
 
-/**
- * Get connections from config.
- *
- * For environment variables:
- * connection env vars must follow the format:
- * SQLPAD_CONNECTIONS__<connectionId>__<connectionFieldName>
- *
- * <connectionId> can be any value to associate a grouping a fields to a connection instance
- * If supplying a connection that was previously defined in the nedb database,
- * this would map internally to connection._id object.
- *
- * <connectionFieldName> should be a field name identified in drivers.
- *
- * To define connections via envvars, `driver` field should be supplied.
- * _id field is not required, as it is defined in second env var fragment.
- *
- * Example: SQLPAD_CONNECTIONS__ab123__sqlserverEncrypt=""
- *
- * From file, resulting parsed configuration from file is expected to follow format `connections.<id>.<fieldname>`
- * {
- *   connections: {
- *     ab123: {
- *       sqlserverEncrypt: true
- *     }
- *   }
- * }
- *
- * @param {object} env
- * @returns {array<object>} arrayOfConnections
- */
-function getConnectionsFromConfig(env = process.env) {
-  // Create a map of connections from parsing environment variable
-  const connectionsMapFromEnv = Object.keys(env)
-    .filter(key => key.startsWith('SQLPAD_CONNECTIONS__'))
-    .reduce((connectionsMap, envVar) => {
-      // eslint-disable-next-line no-unused-vars
-      const [prefix, id, field] = envVar.split('__');
-      if (!connectionsMap[id]) {
-        connectionsMap[id] = {};
-      }
-      connectionsMap[id][field] = env[envVar];
-      return connectionsMap;
-    }, {});
-
-  // Get copy of connections from config file
-  const { connections } = _.cloneDeep(configFromFile);
-
-  // connections key from file matches format that is constructed from env
-  // merge the 2 together then create an array out of them
-  const connectionsMap = { ...connectionsMapFromEnv, ...connections };
-
-  const connectionsFromConfig = [];
-  Object.keys(connectionsMap).forEach(id => {
-    try {
-      let connection = connectionsMap[id];
-      connection._id = id;
-      connection = drivers.validateConnection(connection);
-      connection.editable = false;
-      connectionsFromConfig.push(connection);
-    } catch (error) {
-      logger.error(
-        error,
-        'Environment connection configuration failed for %s',
-        id
-      );
-    }
-  });
-  return connectionsFromConfig;
-}
-
 function decipherConnection(connection) {
   if (connection.username) {
     connection.username = decipher(connection.username);
@@ -90,64 +17,67 @@ function decipherConnection(connection) {
   return connection;
 }
 
-async function findAll() {
-  let connectionsFromDb = await db.connections.find({});
-  connectionsFromDb = connectionsFromDb.map(conn => {
-    conn.editable = true;
-    return decipherConnection(conn);
-  });
+function makeConnections(nedb) {
+  async function findAll() {
+    let connectionsFromDb = await nedb.connections.find({});
+    connectionsFromDb = connectionsFromDb.map(conn => {
+      conn.editable = true;
+      return decipherConnection(conn);
+    });
 
-  const allConnections = connectionsFromDb.concat(getConnectionsFromConfig());
-  return _.sortBy(allConnections, c => c.name.toLowerCase());
-}
-
-async function findOneById(id) {
-  const connection = await db.connections.findOne({ _id: id });
-  if (connection) {
-    connection.editable = true;
-    return decipherConnection(connection);
+    const allConnections = connectionsFromDb.concat(getConnectionsFromConfig());
+    return _.sortBy(allConnections, c => c.name.toLowerCase());
   }
 
-  // If connection was not found in db try env
-  const connectionFromEnv = getConnectionsFromConfig().find(
-    connection => connection._id === id
-  );
+  async function findOneById(id) {
+    const connection = await nedb.connections.findOne({ _id: id });
+    if (connection) {
+      connection.editable = true;
+      return decipherConnection(connection);
+    }
 
-  return connectionFromEnv;
-}
+    // If connection was not found in db try env
+    const connectionFromEnv = getConnectionsFromConfig().find(
+      connection => connection._id === id
+    );
 
-async function removeOneById(id) {
-  return db.connections.remove({ _id: id });
-}
+    return connectionFromEnv;
+  }
 
-async function save(connection) {
-  if (!connection) {
-    throw new Error('connections.save() requires a connection');
+  async function removeOneById(id) {
+    return nedb.connections.remove({ _id: id });
   }
 
-  connection.username = cipher(connection.username || '');
-  connection.password = cipher(connection.password || '');
+  async function save(connection) {
+    if (!connection) {
+      throw new Error('connections.save() requires a connection');
+    }
 
-  if (!connection.createdDate) {
-    connection.createdDate = new Date();
-  }
-  connection.modifiedDate = new Date();
+    connection.username = cipher(connection.username || '');
+    connection.password = cipher(connection.password || '');
 
-  connection = drivers.validateConnection(connection);
-  const { _id } = connection;
+    if (!connection.createdDate) {
+      connection.createdDate = new Date();
+    }
+    connection.modifiedDate = new Date();
+
+    connection = drivers.validateConnection(connection);
+    const { _id } = connection;
 
-  if (_id) {
-    await db.connections.update({ _id }, connection, {});
-    return findOneById(_id);
+    if (_id) {
+      await nedb.connections.update({ _id }, connection, {});
+      return findOneById(_id);
+    }
+    const newDoc = await nedb.connections.insert(connection);
+    return findOneById(newDoc._id);
   }
-  const newDoc = await db.connections.insert(connection);
-  return findOneById(newDoc._id);
+
+  return {
+    findAll,
+    findOneById,
+    removeOneById,
+    save
+  };
 }
 
-module.exports = {
-  findAll,
-  findOneById,
-  removeOneById,
-  save,
-  getConnectionsFromConfig
-};
+module.exports = makeConnections;
diff --git a/server/models/index.js b/server/models/index.js
new file mode 100644
index 00000000..84f66b8b
--- /dev/null
+++ b/server/models/index.js
@@ -0,0 +1,19 @@
+const makeUsers = require('./users');
+const makeSchemaInfo = require('./schemaInfo');
+const makeResultCache = require('./resultCache');
+const makeQueryHistory = require('./queryHistory');
+const makeQueries = require('./queries');
+const makeConnections = require('./connections');
+const makeConnectionAccesses = require('./connectionAccesses');
+
+module.exports = function(nedb) {
+  return {
+    users: makeUsers(nedb),
+    schemaInfo: makeSchemaInfo(nedb),
+    resultCache: makeResultCache(nedb),
+    queryHistory: makeQueryHistory(nedb),
+    queries: makeQueries(nedb),
+    connections: makeConnections(nedb),
+    connectionAccesses: makeConnectionAccesses(nedb)
+  };
+};
diff --git a/server/models/queries.js b/server/models/queries.js
index e70f6be9..7de00321 100644
--- a/server/models/queries.js
+++ b/server/models/queries.js
@@ -1,4 +1,3 @@
-const db = require('../lib/db.js');
 const Joi = require('@hapi/joi');
 
 /*
@@ -44,60 +43,64 @@ const schema = Joi.object({
   lastAccessDate: Joi.date().default(Date.now)
 });
 
-function findOneById(id) {
-  return db.queries.findOne({ _id: id });
-}
+function makeQueries(nedb) {
+  function findOneById(id) {
+    return nedb.queries.findOne({ _id: id });
+  }
 
-function findAll() {
-  return db.queries.find({});
-}
+  function findAll() {
+    return nedb.queries.find({});
+  }
 
-function findByFilter(filter) {
-  return db.queries.find(filter);
-}
+  function findByFilter(filter) {
+    return nedb.queries.find(filter);
+  }
 
-function removeById(id) {
-  return db.queries.remove({ _id: id });
-}
+  function removeById(id) {
+    return nedb.queries.remove({ _id: id });
+  }
 
-/**
- * Save query object
- * returns saved query object
- * @param {object} query
- */
-async function save(query) {
-  query.modifiedDate = new Date();
-  query.lastAccessDate = new Date();
+  /**
+   * Save query object
+   * returns saved query object
+   * @param {object} query
+   */
+  async function save(query) {
+    query.modifiedDate = new Date();
+    query.lastAccessDate = new Date();
 
-  // clean tags if present
-  // sqlpad v1 saved a lot of bad inputs
-  if (Array.isArray(query.tags)) {
-    query.tags = query.tags
-      .filter(tag => {
-        return typeof tag === 'string' && tag.trim() !== '';
-      })
-      .map(tag => {
-        return tag.trim();
+    // clean tags if present
+    // sqlpad v1 saved a lot of bad inputs
+    if (Array.isArray(query.tags)) {
+      query.tags = query.tags
+        .filter(tag => {
+          return typeof tag === 'string' && tag.trim() !== '';
+        })
+        .map(tag => {
+          return tag.trim();
+        });
+    }
+    const joiResult = schema.validate(query);
+    if (joiResult.error) {
+      return Promise.reject(joiResult.error);
+    }
+    if (query._id) {
+      await nedb.queries.update({ _id: query._id }, joiResult.value, {
+        upsert: true
       });
+      return findOneById(query._id);
+    }
+    const newQuery = await nedb.queries.insert(joiResult.value);
+    return newQuery;
   }
-  const joiResult = schema.validate(query);
-  if (joiResult.error) {
-    return Promise.reject(joiResult.error);
-  }
-  if (query._id) {
-    await db.queries.update({ _id: query._id }, joiResult.value, {
-      upsert: true
-    });
-    return findOneById(query._id);
-  }
-  const newQuery = await db.queries.insert(joiResult.value);
-  return newQuery;
+
+  return {
+    findOneById,
+    findAll,
+    findByFilter,
+    removeById,
+    save
+  };
 }
 
-module.exports = {
-  findOneById,
-  findAll,
-  findByFilter,
-  removeById,
-  save
-};
+module.exports = makeQueries;
diff --git a/server/models/queryHistory.js b/server/models/queryHistory.js
index 77177dec..744a0471 100644
--- a/server/models/queryHistory.js
+++ b/server/models/queryHistory.js
@@ -1,4 +1,3 @@
-const db = require('../lib/db.js');
 const Joi = require('@hapi/joi');
 const config = require('../lib/config');
 
@@ -19,52 +18,56 @@ const schema = Joi.object({
   createdDate: Joi.date().default(new Date())
 });
 
-function findOneById(id) {
-  return db.queryHistory.findOne({ _id: id });
-}
+function makeQueryHistory(nedb) {
+  function findOneById(id) {
+    return nedb.queryHistory.findOne({ _id: id });
+  }
 
-async function findAll() {
-  return db.queryHistory
-    .cfind({}, {})
-    .sort({ startTime: -1 })
-    .exec();
-}
+  async function findAll() {
+    return nedb.queryHistory
+      .cfind({}, {})
+      .sort({ startTime: -1 })
+      .exec();
+  }
 
-function findByFilter(filter) {
-  return db.queryHistory
-    .cfind(filter || {}, {})
-    .sort({ startTime: -1 })
-    .limit(config.get('queryHistoryResultMaxRows'))
-    .exec();
-}
+  function findByFilter(filter) {
+    return nedb.queryHistory
+      .cfind(filter || {}, {})
+      .sort({ startTime: -1 })
+      .limit(config.get('queryHistoryResultMaxRows'))
+      .exec();
+  }
 
-async function removeOldEntries() {
-  const days = config.get('queryHistoryRetentionTimeInDays') * 86400 * 1000;
-  const retentionPeriodStartTime = new Date(new Date().getTime() - days);
+  async function removeOldEntries() {
+    const days = config.get('queryHistoryRetentionTimeInDays') * 86400 * 1000;
+    const retentionPeriodStartTime = new Date(new Date().getTime() - days);
 
-  // Compaction function called separately in every ten minutes
-  return db.queryHistory.remove(
-    { createdDate: { $lt: retentionPeriodStartTime } },
-    { multi: true }
-  );
-}
-/**
- * Save queryHistory object
- * returns saved queryHistory object
- * @param {object} queryHistory
- */
-async function save(data) {
-  const joiResult = schema.validate(data);
-  if (joiResult.error) {
-    return Promise.reject(joiResult.error);
+    return nedb.queryHistory.remove(
+      { createdDate: { $lt: retentionPeriodStartTime } },
+      { multi: true }
+    );
   }
-  return db.queryHistory.insert(joiResult.value);
+
+  /**
+   * Save queryHistory object
+   * returns saved queryHistory object
+   * @param {object} queryHistory
+   */
+  async function save(data) {
+    const joiResult = schema.validate(data);
+    if (joiResult.error) {
+      return Promise.reject(joiResult.error);
+    }
+    return nedb.queryHistory.insert(joiResult.value);
+  }
+
+  return {
+    findOneById,
+    findAll,
+    findByFilter,
+    removeOldEntries,
+    save
+  };
 }
 
-module.exports = {
-  findOneById,
-  findAll,
-  findByFilter,
-  removeOldEntries,
-  save
-};
+module.exports = makeQueryHistory;
diff --git a/server/models/resultCache.js b/server/models/resultCache.js
index c8a395eb..195303a6 100644
--- a/server/models/resultCache.js
+++ b/server/models/resultCache.js
@@ -2,7 +2,6 @@ const fs = require('fs');
 const path = require('path');
 const moment = require('moment');
 const sanitize = require('sanitize-filename');
-const db = require('../lib/db.js');
 const logger = require('../lib/logger');
 const xlsx = require('node-xlsx');
 const { parse } = require('json2csv');
@@ -21,32 +20,74 @@ function jsonFilePath(cacheKey) {
   return path.join(dbPath, '/cache/', cacheKey + '.json');
 }
 
-async function findOneByCacheKey(cacheKey) {
-  return db.cache.findOne({ cacheKey });
-}
-
-async function saveResultCache(cacheKey, queryName) {
-  if (!cacheKey) {
-    throw new Error('cacheKey required');
+function makeResultCache(nedb) {
+  async function findOneByCacheKey(cacheKey) {
+    return nedb.cache.findOne({ cacheKey });
   }
-  const EIGHT_HOURS = 1000 * 60 * 60 * 8;
-  const expiration = new Date(Date.now() + EIGHT_HOURS);
-  const modifiedDate = new Date();
 
-  const savedQueryName = sanitize(
-    (queryName || 'SQLPad Query Results') + ' ' + moment().format('YYYY-MM-DD')
-  );
+  async function saveResultCache(cacheKey, queryName) {
+    if (!cacheKey) {
+      throw new Error('cacheKey required');
+    }
+    const EIGHT_HOURS = 1000 * 60 * 60 * 8;
+    const expiration = new Date(Date.now() + EIGHT_HOURS);
+    const modifiedDate = new Date();
+
+    const savedQueryName = sanitize(
+      (queryName || 'SQLPad Query Results') +
+        ' ' +
+        moment().format('YYYY-MM-DD')
+    );
+
+    const doc = {
+      cacheKey,
+      expiration,
+      queryName: savedQueryName,
+      modifiedDate
+    };
+
+    const existing = await findOneByCacheKey(cacheKey);
+    if (!existing) {
+      doc.createdDate = new Date();
+    }
 
-  const doc = { cacheKey, expiration, queryName: savedQueryName, modifiedDate };
+    return nedb.cache.update({ cacheKey }, doc, {
+      upsert: true
+    });
+  }
 
-  const existing = await findOneByCacheKey(cacheKey);
-  if (!existing) {
-    doc.createdDate = new Date();
+  async function removeExpired() {
+    try {
+      const docs = await nedb.cache.find({ expiration: { $lt: new Date() } });
+      for (const doc of docs) {
+        const filepaths = [
+          xlsxFilePath(doc.cacheKey),
+          csvFilePath(doc.cacheKey)
+        ];
+        filepaths.forEach(fp => {
+          if (fs.existsSync(fp)) {
+            fs.unlinkSync(fp);
+          }
+        });
+        // eslint-disable-next-line no-await-in-loop
+        await nedb.cache.remove({ _id: doc._id }, {});
+      }
+    } catch (error) {
+      logger.error(error);
+    }
   }
 
-  return db.cache.update({ cacheKey }, doc, {
-    upsert: true
-  });
+  return {
+    csvFilePath,
+    findOneByCacheKey,
+    jsonFilePath,
+    removeExpired,
+    saveResultCache,
+    writeCsv,
+    writeJson,
+    writeXlsx,
+    xlsxFilePath
+  };
 }
 
 function writeXlsx(cacheKey, queryResult) {
@@ -119,38 +160,4 @@ function writeJson(cacheKey, queryResult) {
   });
 }
 
-/*  Result cache maintenance
-============================================================================== */
-
-async function removeExpired() {
-  try {
-    const docs = await db.cache.find({ expiration: { $lt: new Date() } });
-    for (const doc of docs) {
-      const filepaths = [xlsxFilePath(doc.cacheKey), csvFilePath(doc.cacheKey)];
-      filepaths.forEach(fp => {
-        if (fs.existsSync(fp)) {
-          fs.unlinkSync(fp);
-        }
-      });
-      // eslint-disable-next-line no-await-in-loop
-      await db.cache.remove({ _id: doc._id }, {});
-    }
-  } catch (error) {
-    logger.error(error);
-  }
-}
-
-// Every five minutes check and expire cache
-const FIVE_MINUTES = 1000 * 60 * 5;
-setInterval(removeExpired, FIVE_MINUTES);
-
-module.exports = {
-  csvFilePath,
-  findOneByCacheKey,
-  jsonFilePath,
-  saveResultCache,
-  writeCsv,
-  writeJson,
-  writeXlsx,
-  xlsxFilePath
-};
+module.exports = makeResultCache;
diff --git a/server/models/schemaInfo.js b/server/models/schemaInfo.js
index d85fb910..1ed67548 100644
--- a/server/models/schemaInfo.js
+++ b/server/models/schemaInfo.js
@@ -1,55 +1,56 @@
-const db = require('../lib/db.js');
-
 function getCacheKey(connectionId) {
   return 'schemaCache:' + connectionId;
 }
 
-/**
- * Get schemaInfo for connection id
- * @param {string} connectionId
- */
-async function getSchemaInfo(connectionId) {
-  const cacheKey = getCacheKey(connectionId);
-  const doc = await db.cache.findOne({ cacheKey });
+function makeSchemaInfo(nedb) {
+  /**
+   * Get schemaInfo for connection id
+   * @param {string} connectionId
+   */
+  async function getSchemaInfo(connectionId) {
+    const cacheKey = getCacheKey(connectionId);
+    const doc = await nedb.cache.findOne({ cacheKey });
 
-  if (!doc) {
-    return;
-  }
+    if (!doc) {
+      return;
+    }
 
-  let schemaInfo;
-  try {
-    schemaInfo =
-      typeof doc.schema === 'string' ? JSON.parse(doc.schema) : doc.schema;
-  } catch (error) {
-    // do nothing. valid schema will be updated
-  }
+    let schemaInfo;
+    try {
+      schemaInfo =
+        typeof doc.schema === 'string' ? JSON.parse(doc.schema) : doc.schema;
+    } catch (error) {
+      // do nothing. valid schema will be updated
+    }
 
-  return schemaInfo;
-}
+    return schemaInfo;
+  }
 
-/**
- * Save schemaInfo to cache db object
- * Schema needs to be stringified as JSON
- * Column names could have dots in name (incompatible with nedb)
- * @param {string} connectionId
- * @param {object} schemaInfo
- */
-async function saveSchemaInfo(connectionId, schemaInfo) {
-  const cacheKey = getCacheKey(connectionId);
-  if (schemaInfo && Object.keys(schemaInfo).length) {
-    const schema = JSON.stringify(schemaInfo);
-    const doc = {
-      cacheKey,
-      schema,
-      modifiedDate: new Date()
-    };
-    return db.cache.update({ cacheKey }, doc, {
-      upsert: true
-    });
+  /**
+   * Save schemaInfo to cache db object
+   * Schema needs to be stringified as JSON
+   * Column names could have dots in name (incompatible with nedb)
+   * @param {string} connectionId
+   * @param {object} schemaInfo
+   */
+  async function saveSchemaInfo(connectionId, schemaInfo) {
+    const cacheKey = getCacheKey(connectionId);
+    if (schemaInfo && Object.keys(schemaInfo).length) {
+      const schema = JSON.stringify(schemaInfo);
+      const doc = {
+        cacheKey,
+        schema,
+        modifiedDate: new Date()
+      };
+      return nedb.cache.update({ cacheKey }, doc, {
+        upsert: true
+      });
+    }
   }
+  return {
+    getSchemaInfo,
+    saveSchemaInfo
+  };
 }
 
-module.exports = {
-  getSchemaInfo,
-  saveSchemaInfo
-};
+module.exports = makeSchemaInfo;
diff --git a/server/models/users.js b/server/models/users.js
index c444cde1..ab859bd7 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -1,5 +1,4 @@
 const Joi = require('@hapi/joi');
-const db = require('../lib/db.js');
 const passhash = require('../lib/passhash.js');
 
 const schema = Joi.object({
@@ -21,66 +20,70 @@ const schema = Joi.object({
   signupDate: Joi.date().optional()
 });
 
-async function save(data) {
-  if (!data.email) {
-    throw new Error('email required when saving user');
-  }
+function makeUsers(nedb) {
+  async function save(data) {
+    if (!data.email) {
+      throw new Error('email required when saving user');
+    }
 
-  data.modifiedDate = new Date();
+    data.modifiedDate = new Date();
 
-  if (data.password) {
-    data.passhash = await passhash.getPasshash(data.password);
-  }
+    if (data.password) {
+      data.passhash = await passhash.getPasshash(data.password);
+    }
 
-  const joiResult = schema.validate(data);
-  if (joiResult.error) {
-    return Promise.reject(joiResult.error);
+    const joiResult = schema.validate(data);
+    if (joiResult.error) {
+      return Promise.reject(joiResult.error);
+    }
+
+    await nedb.users.update({ email: data.email }, joiResult.value, {
+      upsert: true
+    });
+    return findOneByEmail(data.email);
   }
 
-  await db.users.update({ email: data.email }, joiResult.value, {
-    upsert: true
-  });
-  return findOneByEmail(data.email);
-}
+  function findOneByEmail(email) {
+    return nedb.users.findOne({ email: { $regex: new RegExp(email, 'i') } });
+  }
 
-function findOneByEmail(email) {
-  return db.users.findOne({ email: { $regex: new RegExp(email, 'i') } });
-}
+  function findOneById(id) {
+    return nedb.users.findOne({ _id: id });
+  }
 
-function findOneById(id) {
-  return db.users.findOne({ _id: id });
-}
+  function findOneByPasswordResetId(passwordResetId) {
+    return nedb.users.findOne({ passwordResetId });
+  }
 
-function findOneByPasswordResetId(passwordResetId) {
-  return db.users.findOne({ passwordResetId });
-}
+  function findAll() {
+    return nedb.users
+      .cfind({}, { password: 0, passhash: 0 })
+      .sort({ email: 1 })
+      .exec();
+  }
 
-function findAll() {
-  return db.users
-    .cfind({}, { password: 0, passhash: 0 })
-    .sort({ email: 1 })
-    .exec();
-}
+  /**
+   * Returns boolean regarding whether admin registration should be open or not
+   * @returns {Promise<boolean>} administrationOpen
+   */
+  async function adminRegistrationOpen() {
+    const doc = await nedb.users.findOne({ role: 'admin' });
+    return !doc;
+  }
 
-/**
- * Returns boolean regarding whether admin registration should be open or not
- * @returns {Promise<boolean>} administrationOpen
- */
-async function adminRegistrationOpen() {
-  const doc = await db.users.findOne({ role: 'admin' });
-  return !doc;
-}
+  function removeById(id) {
+    return nedb.users.remove({ _id: id });
+  }
 
-function removeById(id) {
-  return db.users.remove({ _id: id });
+  return {
+    findOneByEmail,
+    findOneById,
+    findOneByPasswordResetId,
+    findAll,
+    adminRegistrationOpen,
+    removeById,
+    save
+  };
 }
 
-module.exports = {
-  findOneByEmail,
-  findOneById,
-  findOneByPasswordResetId,
-  findAll,
-  adminRegistrationOpen,
-  removeById,
-  save
-};
+module.exports = makeUsers;
diff --git a/server/routes/app.js b/server/routes/app.js
index a63a4e9e..fef8958e 100644
--- a/server/routes/app.js
+++ b/server/routes/app.js
@@ -1,15 +1,16 @@
 const router = require('express').Router();
 const packageJson = require('../package.json');
-const usersUtil = require('../models/users.js');
+const getModels = require('../models');
 const sendError = require('../lib/sendError');
 
 // NOTE: this route needs a wildcard because it is fetched as a relative url
 // from the front-end. The static SPA does not know if sqlpad is mounted at
 // the root of a domain or if there is a base-url provided in the config
 router.get('*/api/app', async (req, res) => {
-  const { config } = req;
+  const { config, nedb } = req;
   try {
-    const adminRegistrationOpen = await usersUtil.adminRegistrationOpen();
+    const models = getModels(nedb);
+    const adminRegistrationOpen = await models.users.adminRegistrationOpen();
     const currentUser =
       req.isAuthenticated() && req.user
         ? {
diff --git a/server/routes/connection-accesses.js b/server/routes/connection-accesses.js
index 1053deea..d8324af4 100644
--- a/server/routes/connection-accesses.js
+++ b/server/routes/connection-accesses.js
@@ -1,8 +1,6 @@
 const router = require('express').Router();
-const connectionAccessesUtil = require('../models/connectionAccesses.js');
 const consts = require('../lib/consts');
-const usersUtil = require('../models/users.js');
-const connectionUtil = require('../models/connections.js');
+const getModels = require('../models');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
@@ -12,13 +10,14 @@ router.get('/api/connection-accesses', mustBeAuthenticated, async function(
   res
 ) {
   try {
-    const { query } = req;
+    const { query, nedb } = req;
+    const models = getModels(nedb);
     const { includeInactives } = query;
     let connectionAccesses;
     if (includeInactives) {
-      connectionAccesses = await connectionAccessesUtil.findAll();
+      connectionAccesses = await models.connectionAccesses.findAll();
     } else {
-      connectionAccesses = await connectionAccessesUtil.findAllActive();
+      connectionAccesses = await models.connectionAccesses.findAllActive();
     }
     return res.json({ connectionAccesses });
   } catch (error) {
@@ -31,7 +30,8 @@ router.get('/api/connection-accesses/:_id', mustBeAuthenticated, async function(
   res
 ) {
   try {
-    const connectionAccess = await connectionAccessesUtil.findOneById(
+    const models = getModels(req.nedb);
+    const connectionAccess = await models.connectionAccesses.findOneById(
       req.params._id
     );
     if (!connectionAccess) {
@@ -45,6 +45,7 @@ router.get('/api/connection-accesses/:_id', mustBeAuthenticated, async function(
 
 // crete active connection access
 router.post('/api/connection-accesses', mustBeAdmin, async function(req, res) {
+  const models = getModels(req.nedb);
   try {
     let user = {
       id: consts.EVERYONE_ID,
@@ -56,7 +57,7 @@ router.post('/api/connection-accesses', mustBeAdmin, async function(req, res) {
     };
 
     if (req.body.userId !== consts.EVERYONE_ID) {
-      user = await usersUtil.findOneById(req.body.userId);
+      user = await models.users.findOneById(req.body.userId);
       if (!user) {
         return sendError(res, null, 'User not exists');
       }
@@ -69,19 +70,19 @@ router.post('/api/connection-accesses', mustBeAdmin, async function(req, res) {
       }
     }
     if (req.body.connectionId !== consts.EVERY_CONNECTION_ID) {
-      connection = await connectionUtil.findOneById(req.body.connectionId);
+      connection = await models.connections.findOneById(req.body.connectionId);
       if (!connection) {
         return sendError(res, null, 'Connection not exists');
       }
     }
-    let activeAccess = await connectionAccessesUtil.findOneActiveByConnectionIdAndUserId(
+    let activeAccess = await models.connectionAccesses.findOneActiveByConnectionIdAndUserId(
       req.body.connectionId,
       req.body.userId
     );
     if (activeAccess) {
       return sendError(res, null, 'User has active access to connection');
     }
-    let connectionAccess = await connectionAccessesUtil.save({
+    let connectionAccess = await models.connectionAccesses.save({
       connectionId: req.body.connectionId,
       connectionName: connection.name,
       userId: req.body.userId,
@@ -102,8 +103,9 @@ router.put('/api/connection-accesses/:_id/expire', mustBeAdmin, async function(
   req,
   res
 ) {
+  const models = getModels(req.nedb);
   try {
-    const connectionAccess = await connectionAccessesUtil.expire(
+    const connectionAccess = await models.connectionAccesses.expire(
       req.params._id
     );
     return res.json({ connectionAccess });
diff --git a/server/routes/connections.js b/server/routes/connections.js
index 842a760b..924def51 100644
--- a/server/routes/connections.js
+++ b/server/routes/connections.js
@@ -1,5 +1,5 @@
 const router = require('express').Router();
-const connections = require('../models/connections.js');
+const getModels = require('../models');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
@@ -11,7 +11,8 @@ function removePassword(connection) {
 
 router.get('/api/connections', mustBeAuthenticated, async function(req, res) {
   try {
-    const docs = await connections.findAll();
+    const models = getModels(req.nedb);
+    const docs = await models.connections.findAll();
     return res.json({
       connections: docs.map(removePassword)
     });
@@ -25,7 +26,8 @@ router.get('/api/connections/:_id', mustBeAuthenticated, async function(
   res
 ) {
   try {
-    const connection = await connections.findOneById(req.params._id);
+    const models = getModels(req.nedb);
+    const connection = await models.connections.findOneById(req.params._id);
     if (!connection) {
       return sendError(res, null, 'Connection not found');
     }
@@ -39,7 +41,8 @@ router.get('/api/connections/:_id', mustBeAuthenticated, async function(
 
 router.post('/api/connections', mustBeAdmin, async function(req, res) {
   try {
-    const newConnection = await connections.save(req.body);
+    const models = getModels(req.nedb);
+    const newConnection = await models.connections.save(req.body);
     return res.json({
       connection: removePassword(newConnection)
     });
@@ -50,12 +53,13 @@ router.post('/api/connections', mustBeAdmin, async function(req, res) {
 
 router.put('/api/connections/:_id', mustBeAdmin, async function(req, res) {
   try {
-    let connection = await connections.findOneById(req.params._id);
+    const models = getModels(req.nedb);
+    let connection = await models.connections.findOneById(req.params._id);
     if (!connection) {
       return sendError(res, null, 'Connection not found');
     }
     Object.assign(connection, req.body);
-    connection = await connections.save(connection);
+    connection = await models.connections.save(connection);
     return res.json({
       connection: removePassword(connection)
     });
@@ -66,7 +70,8 @@ router.put('/api/connections/:_id', mustBeAdmin, async function(req, res) {
 
 router.delete('/api/connections/:_id', mustBeAdmin, async function(req, res) {
   try {
-    await connections.removeOneById(req.params._id);
+    const models = getModels(req.nedb);
+    await models.connections.removeOneById(req.params._id);
     return res.json({});
   } catch (error) {
     sendError(res, error, 'Problem deleting connection');
diff --git a/server/routes/download-results.js b/server/routes/download-results.js
index 30c8dc7d..e12d4afe 100644
--- a/server/routes/download-results.js
+++ b/server/routes/download-results.js
@@ -1,13 +1,14 @@
 const fs = require('fs');
 const router = require('express').Router();
-const resultCache = require('../models/resultCache.js');
+const getModels = require('../models');
 const logger = require('../lib/logger');
 
 router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
   const { cacheKey } = req.params;
+  const models = getModels(req.nedb);
   try {
     if (req.config.get('allowCsvDownload')) {
-      const cache = await resultCache.findOneByCacheKey(cacheKey);
+      const cache = await models.resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
       }
@@ -17,7 +18,7 @@ router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
         'attachment; filename="' + encodeURIComponent(filename) + '"'
       );
       res.setHeader('Content-Type', 'text/csv');
-      fs.createReadStream(resultCache.csvFilePath(cacheKey)).pipe(res);
+      fs.createReadStream(models.resultCache.csvFilePath(cacheKey)).pipe(res);
     } else {
       return next(new Error('CSV download disabled'));
     }
@@ -30,9 +31,10 @@ router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
 
 router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
   const { cacheKey } = req.params;
+  const models = getModels(req.nedb);
   try {
     if (req.config.get('allowCsvDownload')) {
-      const cache = await resultCache.findOneByCacheKey(cacheKey);
+      const cache = await models.resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
       }
@@ -45,7 +47,7 @@ router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
         'Content-Type',
         'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
       );
-      fs.createReadStream(resultCache.xlsxFilePath(cacheKey)).pipe(res);
+      fs.createReadStream(models.resultCache.xlsxFilePath(cacheKey)).pipe(res);
     } else {
       return next(new Error('XLSX download disabled'));
     }
@@ -58,9 +60,10 @@ router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
 
 router.get('/download-results/:cacheKey.json', async function(req, res, next) {
   const { cacheKey } = req.params;
+  const models = getModels(req.nedb);
   try {
     if (req.config.get('allowCsvDownload')) {
-      const cache = await resultCache.findOneByCacheKey(cacheKey);
+      const cache = await models.resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
       }
@@ -70,7 +73,7 @@ router.get('/download-results/:cacheKey.json', async function(req, res, next) {
         'attachment; filename="' + encodeURIComponent(filename) + '"'
       );
       res.setHeader('Content-Type', 'application/json');
-      fs.createReadStream(resultCache.jsonFilePath(cacheKey)).pipe(res);
+      fs.createReadStream(models.resultCache.jsonFilePath(cacheKey)).pipe(res);
     } else {
       return next(new Error('JSON download disabled'));
     }
diff --git a/server/routes/forgot-password.js b/server/routes/forgot-password.js
index 26d34513..584b0ce3 100644
--- a/server/routes/forgot-password.js
+++ b/server/routes/forgot-password.js
@@ -1,11 +1,12 @@
 const router = require('express').Router();
 const uuid = require('uuid');
-const usersUtil = require('../models/users.js');
+const getModels = require('../models');
 const makeEmail = require('../lib/email');
 const sendError = require('../lib/sendError');
 const logger = require('../lib/logger');
 
 router.post('/api/forgot-password', async function(req, res) {
+  const models = getModels(req.nedb);
   if (!req.body.email) {
     return sendError(res, null, 'Email address must be provided');
   }
@@ -16,7 +17,7 @@ router.post('/api/forgot-password', async function(req, res) {
   const email = makeEmail(req.config);
 
   try {
-    const user = await usersUtil.findOneByEmail(req.body.email);
+    const user = await models.users.findOneByEmail(req.body.email);
 
     // If user not found send success regardless
     // This is not a user-validation service
@@ -26,7 +27,7 @@ router.post('/api/forgot-password', async function(req, res) {
 
     user.passwordResetId = uuid.v4();
 
-    await usersUtil.save(user);
+    await models.users.save(user);
 
     // Send email, but do not block response
     const resetPath = `/password-reset/${user.passwordResetId}`;
diff --git a/server/routes/oauth.js b/server/routes/oauth.js
index 6d194fa6..69d15f5a 100644
--- a/server/routes/oauth.js
+++ b/server/routes/oauth.js
@@ -3,7 +3,8 @@ const PassportGoogleStrategy = require('passport-google-oauth20').Strategy;
 const router = require('express').Router();
 const logger = require('../lib/logger');
 const checkWhitelist = require('../lib/check-whitelist.js');
-const usersUtil = require('../models/users.js');
+const { getNedb } = require('../lib/db');
+const getModels = require('../models');
 
 /**
  * Adds Google auth strategy and adds Google auth routes if Google auth is configured
@@ -29,21 +30,24 @@ function makeGoogleAuth(config) {
       });
     }
 
+    const nedb = await getNedb();
+    const models = getModels(nedb);
+
     try {
       let [openAdminRegistration, user] = await Promise.all([
-        usersUtil.adminRegistrationOpen(),
-        usersUtil.findOneByEmail(email)
+        models.users.adminRegistrationOpen(),
+        models.users.findOneByEmail(email)
       ]);
 
       if (user) {
         user.signupDate = new Date();
-        const newUser = await usersUtil.save(user);
+        const newUser = await models.users.save(user);
         newUser.id = newUser._id;
         return done(null, newUser);
       }
       const whitelistedDomains = config.get('whitelistedDomains');
       if (openAdminRegistration || checkWhitelist(whitelistedDomains, email)) {
-        const newUser = await usersUtil.save({
+        const newUser = await models.users.save({
           email,
           role: openAdminRegistration ? 'admin' : 'editor',
           signupDate: new Date()
diff --git a/server/routes/password-reset.js b/server/routes/password-reset.js
index 38360a31..33273728 100644
--- a/server/routes/password-reset.js
+++ b/server/routes/password-reset.js
@@ -1,11 +1,12 @@
 const router = require('express').Router();
-const usersUtil = require('../models/users.js');
+const getModels = require('../models');
 const sendError = require('../lib/sendError');
 
 // This route used to set new password given a passwordResetId
 router.post('/api/password-reset/:passwordResetId', async function(req, res) {
   try {
-    const user = await usersUtil.findOneByPasswordResetId(
+    const models = getModels(req.nedb);
+    const user = await models.users.findOneByPasswordResetId(
       req.params.passwordResetId
     );
 
@@ -20,7 +21,7 @@ router.post('/api/password-reset/:passwordResetId', async function(req, res) {
     }
     user.password = req.body.password;
     user.passwordResetId = '';
-    await usersUtil.save(user);
+    await models.users.save(user);
     return res.json({});
   } catch (error) {
     sendError(res, error, 'Problem querying user database');
diff --git a/server/routes/queries.js b/server/routes/queries.js
index e2af1cba..0f6cdab2 100644
--- a/server/routes/queries.js
+++ b/server/routes/queries.js
@@ -1,5 +1,5 @@
 const router = require('express').Router();
-const queriesUtil = require('../models/queries.js');
+const getModels = require('../models');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const mustBeAuthenticatedOrChartLink = require('../middleware/must-be-authenticated-or-chart-link-noauth.js');
 const sendError = require('../lib/sendError');
@@ -27,7 +27,8 @@ router.delete('/api/queries/:_id', mustBeAuthenticated, async function(
   res
 ) {
   try {
-    await queriesUtil.removeById(req.params._id);
+    const models = getModels(req.nedb);
+    await models.queries.removeById(req.params._id);
     return res.json({});
   } catch (error) {
     sendError(res, error, 'Problem deleting query');
@@ -36,7 +37,8 @@ router.delete('/api/queries/:_id', mustBeAuthenticated, async function(
 
 router.get('/api/queries', mustBeAuthenticated, async function(req, res) {
   try {
-    const queries = await queriesUtil.findAll();
+    const models = getModels(req.nedb);
+    const queries = await models.queries.findAll();
     return res.json({ queries });
   } catch (error) {
     sendError(res, error, 'Problem querying query database');
@@ -48,7 +50,8 @@ router.get('/api/queries/:_id', mustBeAuthenticatedOrChartLink, async function(
   res
 ) {
   try {
-    const query = await queriesUtil.findOneById(req.params._id);
+    const models = getModels(req.nedb);
+    const query = await models.queries.findOneById(req.params._id);
     if (!query) {
       return res.json({
         query: {}
@@ -75,7 +78,8 @@ router.post('/api/queries', mustBeAuthenticated, async function(req, res) {
   };
 
   try {
-    const newQuery = await queriesUtil.save(query);
+    const models = getModels(req.nedb);
+    const newQuery = await models.queries.save(query);
     // This is async, but save operation doesn't care about when/if finished
     pushQueryToSlack(req.config, newQuery);
     return res.json({
@@ -88,7 +92,8 @@ router.post('/api/queries', mustBeAuthenticated, async function(req, res) {
 
 router.put('/api/queries/:_id', mustBeAuthenticated, async function(req, res) {
   try {
-    const query = await queriesUtil.findOneById(req.params._id);
+    const models = getModels(req.nedb);
+    const query = await models.queries.findOneById(req.params._id);
     if (!query) {
       return sendError(res, null, 'Query not found');
     }
@@ -111,7 +116,7 @@ router.put('/api/queries/:_id', mustBeAuthenticated, async function(req, res) {
       modifiedBy: email
     });
 
-    const newQuery = await queriesUtil.save(query);
+    const newQuery = await models.queries.save(query);
     return res.json({ query: newQuery });
   } catch (error) {
     sendError(res, error, 'Problem saving query');
diff --git a/server/routes/query-history.js b/server/routes/query-history.js
index 7c3f5dd2..16f12bd2 100644
--- a/server/routes/query-history.js
+++ b/server/routes/query-history.js
@@ -1,13 +1,15 @@
 const router = require('express').Router();
 const uuid = require('uuid');
 const getMeta = require('../lib/getMeta');
-const queryHistoryUtil = require('../models/queryHistory.js');
+const getModels = require('../models');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const urlFilterToNeDbFilter = require('../lib/urlFilterToNeDbFilter');
 const sendError = require('../lib/sendError');
 
 router.get('/api/query-history', mustBeAuthenticated, async function(req, res) {
   try {
+    const models = getModels(req.nedb);
+
     const queryHistory = {
       id: uuid.v4(),
       cacheKey: null,
@@ -22,7 +24,7 @@ router.get('/api/query-history', mustBeAuthenticated, async function(req, res) {
 
     // Convert URL filter to NeDB compatible filter object
     const dbFilter = urlFilterToNeDbFilter(req.query.filter);
-    const dbQueryHistory = await queryHistoryUtil.findByFilter(dbFilter);
+    const dbQueryHistory = await models.queryHistory.findByFilter(dbFilter);
 
     dbQueryHistory.map(q => {
       delete q._id;
@@ -51,7 +53,10 @@ router.get('/api/query-history/:_id', mustBeAuthenticated, async function(
   res
 ) {
   try {
-    const queryHistoryItem = await queryHistoryUtil.findOneById(req.params._id);
+    const models = getModels(req.nedb);
+    const queryHistoryItem = await models.queryHistory.findOneById(
+      req.params._id
+    );
     if (!queryHistoryItem) {
       return sendError(res, null, 'Query history item not found');
     }
diff --git a/server/routes/query-result.js b/server/routes/query-result.js
index 3a17a51b..a5d35fee 100644
--- a/server/routes/query-result.js
+++ b/server/routes/query-result.js
@@ -1,9 +1,6 @@
 const router = require('express').Router();
 const { runQuery } = require('../drivers/index');
-const connections = require('../models/connections.js');
-const resultCache = require('../models/resultCache.js');
-const queriesUtil = require('../models/queries.js');
-const queryHistory = require('../models/queryHistory');
+const getModels = require('../models');
 const mustHaveConnectionAccess = require('../middleware/must-have-connection-access.js');
 const mustHaveConnectionAccessOrChartLink = require('../middleware/must-have-connection-access-or-chart-link-noauth');
 const sendError = require('../lib/sendError');
@@ -15,7 +12,8 @@ router.get(
   mustHaveConnectionAccessOrChartLink,
   async function(req, res) {
     try {
-      const query = await queriesUtil.findOneById(req.params._queryId);
+      const models = getModels(req.nedb);
+      const query = await models.queries.findOneById(req.params._queryId);
       if (!query) {
         return sendError(res, null, 'Query not found (save query first)');
       }
@@ -64,8 +62,9 @@ router.post('/api/query-result', mustHaveConnectionAccess, async function(
 });
 
 async function getQueryResult(req, data) {
+  const models = getModels(req.nedb);
   const { connectionId, cacheKey, queryId, queryName, queryText, user } = data;
-  const connection = await connections.findOneById(connectionId);
+  const connection = await models.connections.findOneById(connectionId);
 
   if (!connection) {
     throw new Error('Please choose a connection');
@@ -76,8 +75,8 @@ async function getQueryResult(req, data) {
   queryResult.cacheKey = cacheKey;
 
   if (req.config.get('queryHistoryRetentionTimeInDays') > 0) {
-    await queryHistory.removeOldEntries();
-    await queryHistory.save({
+    await models.queryHistory.removeOldEntries();
+    await models.queryHistory.save({
       userId: user._id,
       userEmail: user.email,
       connectionId: connection._id,
@@ -94,10 +93,10 @@ async function getQueryResult(req, data) {
   }
 
   if (req.config.get('allowCsvDownload')) {
-    resultCache.saveResultCache(cacheKey, queryName);
-    await resultCache.writeXlsx(cacheKey, queryResult);
-    await resultCache.writeCsv(cacheKey, queryResult);
-    await resultCache.writeJson(cacheKey, queryResult);
+    models.resultCache.saveResultCache(cacheKey, queryName);
+    await models.resultCache.writeXlsx(cacheKey, queryResult);
+    await models.resultCache.writeCsv(cacheKey, queryResult);
+    await models.resultCache.writeJson(cacheKey, queryResult);
   }
 
   return queryResult;
diff --git a/server/routes/saml.js b/server/routes/saml.js
index c23b60ec..0ec0755a 100644
--- a/server/routes/saml.js
+++ b/server/routes/saml.js
@@ -2,7 +2,8 @@ const passport = require('passport');
 const SamlStrategy = require('passport-saml').Strategy;
 const router = require('express').Router();
 const logger = require('../lib/logger');
-const usersUtil = require('../models/users.js');
+const { getNedb } = require('../lib/db');
+const getModels = require('../models');
 
 /**
  * Adds passport SAML strategy and SAML auth routes if SAML is configured
@@ -41,7 +42,9 @@ function makeSamlAuth(config) {
             p[
               'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
             ];
-          const user = await usersUtil.findOneByEmail(email);
+          const nedb = await getNedb();
+          const models = getModels(nedb);
+          const user = await models.users.findOneByEmail(email);
           logger.info('User logged in via SAML %s', email);
           if (!user) {
             return done(null, false);
diff --git a/server/routes/schema-info.js b/server/routes/schema-info.js
index 540a1760..b478e72e 100644
--- a/server/routes/schema-info.js
+++ b/server/routes/schema-info.js
@@ -1,6 +1,5 @@
 const router = require('express').Router();
-const connections = require('../models/connections');
-const schemaInfoUtil = require('../models/schemaInfo.js');
+const getModels = require('../models');
 const driver = require('../drivers');
 const mustHaveConnectionAccess = require('../middleware/must-have-connection-access.js');
 const sendError = require('../lib/sendError');
@@ -9,17 +8,18 @@ router.get(
   '/api/schema-info/:connectionId',
   mustHaveConnectionAccess,
   async function(req, res) {
+    const models = getModels(req.nedb);
     const { connectionId } = req.params;
     const reload = req.query.reload === 'true';
 
     try {
-      const conn = await connections.findOneById(connectionId);
+      const conn = await models.connections.findOneById(connectionId);
 
       if (!conn) {
         throw new Error('Connection not found');
       }
 
-      let schemaInfo = await schemaInfoUtil.getSchemaInfo(connectionId);
+      let schemaInfo = await models.schemaInfo.getSchemaInfo(connectionId);
 
       if (schemaInfo && !reload) {
         return res.json({ schemaInfo });
@@ -27,7 +27,7 @@ router.get(
 
       schemaInfo = await driver.getSchema(conn);
       if (Object.keys(schemaInfo).length) {
-        await schemaInfoUtil.saveSchemaInfo(connectionId, schemaInfo);
+        await models.schemaInfo.saveSchemaInfo(connectionId, schemaInfo);
       }
       return res.json({ schemaInfo });
     } catch (error) {
diff --git a/server/routes/signup-signin-signout.js b/server/routes/signup-signin-signout.js
index 9329a2f7..a9ad3031 100644
--- a/server/routes/signup-signin-signout.js
+++ b/server/routes/signup-signin-signout.js
@@ -3,13 +3,15 @@ const PassportLocalStrategy = require('passport-local').Strategy;
 const BasicStrategy = require('passport-http').BasicStrategy;
 const router = require('express').Router();
 const checkWhitelist = require('../lib/check-whitelist');
-const usersUtil = require('../models/users.js');
+const getModels = require('../models');
+const { getNedb } = require('../lib/db');
 const sendError = require('../lib/sendError');
 const logger = require('../lib/logger');
 const passhash = require('../lib/passhash.js');
 
 async function handleSignup(req, res, next) {
   try {
+    const models = getModels(req.nedb);
     const whitelistedDomains = req.config.get('whitelistedDomains');
 
     if (req.body.password !== req.body.passwordConfirmation) {
@@ -17,8 +19,8 @@ async function handleSignup(req, res, next) {
     }
 
     let [user, adminRegistrationOpen] = await Promise.all([
-      usersUtil.findOneByEmail(req.body.email),
-      usersUtil.adminRegistrationOpen()
+      models.users.findOneByEmail(req.body.email),
+      models.users.adminRegistrationOpen()
     ]);
 
     if (user && user.passhash) {
@@ -28,7 +30,7 @@ async function handleSignup(req, res, next) {
     if (user) {
       user.password = req.body.password;
       user.signupDate = new Date();
-      await usersUtil.save(user);
+      await models.users.save(user);
       return next();
     }
 
@@ -38,7 +40,7 @@ async function handleSignup(req, res, next) {
       adminRegistrationOpen ||
       checkWhitelist(whitelistedDomains, req.body.email)
     ) {
-      user = await usersUtil.save({
+      user = await models.users.save({
         email: req.body.email,
         password: req.body.password,
         role: adminRegistrationOpen ? 'admin' : 'editor',
@@ -74,7 +76,9 @@ function makeLocalAuth(config) {
         },
         async function passportLocalStrategyHandler(email, password, done) {
           try {
-            const user = await usersUtil.findOneByEmail(email);
+            const nedb = await getNedb();
+            const models = getModels(nedb);
+            const user = await models.users.findOneByEmail(email);
             if (!user) {
               return done(null, false, { message: 'wrong email or password' });
             }
@@ -103,7 +107,9 @@ function makeLocalAuth(config) {
     passport.use(
       new BasicStrategy(async function(username, password, callback) {
         try {
-          const user = await usersUtil.findOneByEmail(username);
+          const nedb = await getNedb();
+          const models = getModels(nedb);
+          const user = await models.users.findOneByEmail(username);
           if (!user) {
             return callback(null, false);
           }
diff --git a/server/routes/tags.js b/server/routes/tags.js
index 3a2aab2a..98584d22 100644
--- a/server/routes/tags.js
+++ b/server/routes/tags.js
@@ -1,12 +1,13 @@
 const _ = require('lodash');
 const router = require('express').Router();
-const queriesUtil = require('../models/queries.js');
+const getModels = require('../models');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
 
 router.get('/api/tags', mustBeAuthenticated, async function(req, res) {
   try {
-    const queries = await queriesUtil.findAll();
+    const models = getModels(req.nedb);
+    const queries = await models.queries.findAll();
     const tags = _.uniq(_.flatten(_.map(queries, 'tags')))
       .sort()
       .filter(t => t);
diff --git a/server/routes/users.js b/server/routes/users.js
index 7fbd2b8a..662bb8c7 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -1,5 +1,5 @@
 const router = require('express').Router();
-const usersUtil = require('../models/users.js');
+const getModels = require('../models');
 const makeEmail = require('../lib/email');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
@@ -8,7 +8,8 @@ const logger = require('../lib/logger');
 
 router.get('/api/users', mustBeAuthenticated, async function(req, res) {
   try {
-    const users = await usersUtil.findAll();
+    const models = getModels(req.nedb);
+    const users = await models.users.findAll();
     return res.json({ users });
   } catch (error) {
     sendError(res, error, 'Problem getting uers');
@@ -18,11 +19,12 @@ router.get('/api/users', mustBeAuthenticated, async function(req, res) {
 // create/whitelist/invite user
 router.post('/api/users', mustBeAdmin, async function(req, res) {
   try {
-    let user = await usersUtil.findOneByEmail(req.body.email);
+    const models = getModels(req.nedb);
+    let user = await models.users.findOneByEmail(req.body.email);
     if (user) {
       return sendError(res, null, 'User already exists');
     }
-    user = await usersUtil.save({
+    user = await models.users.save({
       email: req.body.email.toLowerCase(),
       role: req.body.role
     });
@@ -44,7 +46,8 @@ router.put('/api/users/:_id', mustBeAdmin, async function(req, res) {
     return sendError(res, null, "You can't unadmin yourself");
   }
   try {
-    const updateUser = await usersUtil.findOneById(params._id);
+    const models = getModels(req.nedb);
+    const updateUser = await models.users.findOneById(params._id);
     if (!updateUser) {
       return sendError(res, null, 'user not found');
     }
@@ -56,7 +59,7 @@ router.put('/api/users/:_id', mustBeAdmin, async function(req, res) {
     if (body.passwordResetId != null) {
       updateUser.passwordResetId = body.passwordResetId;
     }
-    const updatedUser = await usersUtil.save(updateUser);
+    const updatedUser = await models.users.save(updateUser);
     return res.json({ user: updatedUser });
   } catch (error) {
     sendError(res, error, 'Problem saving user');
@@ -68,7 +71,8 @@ router.delete('/api/users/:_id', mustBeAdmin, async function(req, res) {
     return sendError(res, null, "You can't delete yourself");
   }
   try {
-    await usersUtil.removeById(req.params._id);
+    const models = getModels(req.nedb);
+    await models.users.removeById(req.params._id);
     return res.json({});
   } catch (error) {
     sendError(res, error, 'Problem deleting user');
diff --git a/server/server.js b/server/server.js
index f4296d7f..f9da527f 100755
--- a/server/server.js
+++ b/server/server.js
@@ -4,12 +4,16 @@ const fs = require('fs');
 const http = require('http');
 const https = require('https');
 const detectPort = require('detect-port');
+const makeApp = require('./app');
 
 // Parse command line flags to see if anything special needs to happen
 require('./lib/cli-flow.js');
 
 const logger = require('./lib/logger');
 const config = require('./lib/config');
+const { makeNedb } = require('./lib/db');
+
+const dbPromise = makeNedb(config);
 
 const configValidations = config.getValidations();
 configValidations.warnings.map(warning => logger.warn(warning));
@@ -18,8 +22,6 @@ if (configValidations.errors.length > 0) {
   process.exit(1);
 }
 
-const app = require('./app')(config);
-
 const baseUrl = config.get('baseUrl');
 const ip = config.get('ip');
 const port = config.get('port');
@@ -30,8 +32,6 @@ const certPath = config.get('certPath');
 const systemdSocket = config.get('systemdSocket');
 const timeoutSeconds = config.get('timeoutSeconds');
 
-const db = require('./lib/db');
-
 function isFdObject(ob) {
   return ob && typeof ob.fd === 'number';
 }
@@ -70,7 +70,9 @@ function detectPortOrSystemd(port) {
 ============================================================================= */
 let server;
 
-async function startServer() {
+async function startServer(nedb) {
+  const app = makeApp(config, nedb);
+
   // determine if key pair exists for certs
   if (keyPath && certPath) {
     // https only
@@ -122,10 +124,12 @@ async function startServer() {
   server.setTimeout(timeoutSeconds * 1000);
 }
 
-db.loadPromise.then(startServer).catch(error => {
-  logger.error(error, 'Error starting SQLPad');
-  process.exit(1);
-});
+dbPromise
+  .then(nedb => startServer(nedb))
+  .catch(error => {
+    logger.error(error, 'Error starting SQLPad');
+    process.exit(1);
+  });
 
 function handleShutdownSignal(signal) {
   if (!server) {


Test patch:
diff --git a/server/test/api/password-reset.js b/server/test/api/password-reset.js
index db4de75b..9c2e9521 100644
--- a/server/test/api/password-reset.js
+++ b/server/test/api/password-reset.js
@@ -1,13 +1,16 @@
 const assert = require('assert');
 const utils = require('../utils');
 const uuid = require('uuid');
-const usersUtil = require('../../models/users');
+const { getNedb } = require('../../lib/db');
+const getModels = require('../../models');
 
 async function setReset() {
-  const user = await usersUtil.findOneByEmail('admin@test.com');
+  const nedb = await getNedb();
+  const models = getModels(nedb);
+  const user = await models.users.findOneByEmail('admin@test.com');
   const passwordResetId = uuid.v4();
   user.passwordResetId = passwordResetId;
-  await usersUtil.save(user);
+  await models.users.save(user);
   return passwordResetId;
 }
 
diff --git a/server/test/models/connections.js b/server/test/models/connections.js
index b37418a8..ec47afee 100644
--- a/server/test/models/connections.js
+++ b/server/test/models/connections.js
@@ -1,14 +1,24 @@
 const assert = require('assert');
-const connections = require('../../models/connections.js');
+const { getNedb } = require('../../lib/db');
+const { getConnectionsFromConfig } = require('../../lib/connectionsFromConfig');
+const getModels = require('../../models');
 
 describe('getConnectionsFromConfig', function() {
+  let nedb;
+  let models;
+
+  before(async function() {
+    nedb = await getNedb();
+    models = getModels(nedb);
+  });
+
   it('handles empty object', function() {
-    const cs = connections.getConnectionsFromConfig({});
+    const cs = getConnectionsFromConfig({});
     assert(Array.isArray(cs));
   });
 
   it('skips partial connections', function() {
-    const cs = connections.getConnectionsFromConfig({
+    const cs = getConnectionsFromConfig({
       SQLPAD_CONNECTIONS__abc__driver: 'postgres'
     });
     assert(Array.isArray(cs));
@@ -16,7 +26,7 @@ describe('getConnectionsFromConfig', function() {
   });
 
   it('parses connection properly', function() {
-    const cs = connections.getConnectionsFromConfig({
+    const cs = getConnectionsFromConfig({
       SQLPAD_CONNECTIONS__abc__driver: 'postgres',
       SQLPAD_CONNECTIONS__abc__name: 'env-postgres',
       SQLPAD_CONNECTIONS__abc__host: 'localhost',
@@ -42,7 +52,7 @@ describe('getConnectionsFromConfig', function() {
     process.env.SQLPAD_CONNECTIONS__abc__port = '5432';
     process.env.SQLPAD_CONNECTIONS__abc__postgresSsl = 'true';
 
-    const allConnections = await connections.findAll();
+    const allConnections = await models.connections.findAll();
     const connection = allConnections.find(c => c._id === 'abc');
     assert.strictEqual(connection.name, 'env-postgres', 'connection.name');
     assert.strictEqual(connection.editable, false, 'connection.editable');
@@ -61,7 +71,7 @@ describe('getConnectionsFromConfig', function() {
     process.env.SQLPAD_CONNECTIONS__abc__port = '5432';
     process.env.SQLPAD_CONNECTIONS__abc__postgresSsl = 'true';
 
-    const connection = await connections.findOneById('abc');
+    const connection = await models.connections.findOneById('abc');
     assert.strictEqual(connection.name, 'env-postgres', 'connection.name');
     assert.strictEqual(connection.editable, false, 'connection.editable');
 
diff --git a/server/test/utils.js b/server/test/utils.js
index b45b3c63..c9cdf17d 100644
--- a/server/test/utils.js
+++ b/server/test/utils.js
@@ -1,10 +1,13 @@
 const assert = require('assert');
 const request = require('supertest');
-const usersUtil = require('../models/users');
+const getModels = require('../models');
 const consts = require('../lib/consts');
-const db = require('../lib/db');
 const config = require('../lib/config');
-const app = require('../app')(config);
+const { makeNedb } = require('../lib/db');
+const makeApp = require('../app');
+
+const dbPromise = makeNedb(config);
+let app;
 
 const users = {
   admin: {
@@ -25,13 +28,14 @@ function expectKeys(data, expectedKeys) {
   );
 }
 
-function reset() {
+async function reset() {
+  const nedb = await dbPromise;
   return Promise.all([
-    db.users.remove({}, { multi: true }),
-    db.queries.remove({}, { multi: true }),
-    db.queryHistory.remove({}, { multi: true }),
-    db.connections.remove({}, { multi: true }),
-    db.connectionAccesses.remove(
+    nedb.users.remove({}, { multi: true }),
+    nedb.queries.remove({}, { multi: true }),
+    nedb.queryHistory.remove({}, { multi: true }),
+    nedb.connections.remove({}, { multi: true }),
+    nedb.connectionAccesses.remove(
       {
         $not: {
           $and: [
@@ -47,8 +51,10 @@ function reset() {
 
 async function resetWithUser() {
   await reset();
+  const nedb = await dbPromise;
+  const models = getModels(nedb);
   const saves = Object.keys(users).map(key => {
-    return usersUtil.save(users[key]);
+    return models.users.save(users[key]);
   });
   return Promise.all(saves);
 }
@@ -63,6 +69,7 @@ function addAuth(req, role) {
 }
 
 async function del(role, url, statusCode = 200) {
+  await dbPromise;
   let req = request(app).delete(url);
   req = addAuth(req, role);
   const response = await req.expect(statusCode);
@@ -70,6 +77,7 @@ async function del(role, url, statusCode = 200) {
 }
 
 async function get(role, url, statusCode = 200) {
+  await dbPromise;
   let req = request(app).get(url);
   req = addAuth(req, role);
   const response = await req.expect(statusCode);
@@ -77,6 +85,7 @@ async function get(role, url, statusCode = 200) {
 }
 
 async function post(role, url, body, statusCode = 200) {
+  await dbPromise;
   let req = request(app).post(url);
   req = addAuth(req, role);
   const response = await req.send(body).expect(statusCode);
@@ -84,16 +93,21 @@ async function post(role, url, body, statusCode = 200) {
 }
 
 async function put(role, url, body, statusCode = 200) {
+  await dbPromise;
   let req = request(app).put(url);
   req = addAuth(req, role);
   const response = await req.send(body).expect(statusCode);
   return response.body;
 }
 
-// Sometimes config test is flaky because API returns a forbidden.
-// Is it because db is not ready? This will ensure that for tests.
 before(async function() {
-  await db.loadPromise;
+  const nedb = await dbPromise;
+  app = makeApp(config, nedb);
+
+  assert.throws(() => {
+    makeNedb(config);
+  }, 'ensure nedb can be made once');
+
   return resetWithUser();
 });