Repository: rickbergfalk/sqlpad
Pull request #454 (2019-08-03 18:12:27)

Add INI/JSON config file support
Adds ability to provide a JSON or INI config file path via cli flag `--config` or env var `SQLPAD_CONFIG`. 

Old JSON config files in `$HOME/.sqlpadrc` are somewhat supported. If the saved file uses the `cliFlag` field instead of the `key` field from [configItems.js](https://github.com/rickbergfalk/sqlpad/blob/master/server/lib/config/configItems.js) the value will not be recognized. A warning will print to console.

`--save` and `--forget` cli flags have also been removed with this change.

Default dbPath/config file path are also removed. Both must be provided explicitly


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5671065..56fd1385 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,27 @@
 
 ### WIP
 
+#### Breaking changes
+
+- Default db path is no longer used if db path is not provided in config. Previous default was `$HOME/sqlpad/db`.
+- Default config file path no longer used. Previous default was `$HOME/.sqlpadrc`.
+- Configuration UI has been removed. See https://github.com/rickbergfalk/sqlpad/issues/447.
+- cli-flags in saved .sqlpadrc JSON are no longer used for config values. These configuration keys should instead be switched the the `key` found in `configItems.js`. For example, instead of `dir` or `db`, use `dbPath`. Instead of `cert-passphrase` use `certPassphrase`, etc.
+- `--save` and `--forget` cli flags no longer supported
+
+#### Features
+
+- SAML authentication support
+- Remember selected connection id / schema toggle
+- Support for JSON and INI config file added. File should config using `key` fields found in [configItems.js](https://github.com/rickbergfalk/sqlpad/blob/master/server/lib/config/configItems.js). Config file path default is `$HOME/.sqlpadrc` and may otherwise be specified using `--config` via command line or `SQLPAD_CONFIG` environment variable.
+
+#### Fixes
+
 - Fix preview sticking around after query selection
 - Fix tag/search input in query list
-- Remember selected connection id / schema toggle
-- Loading indicator for schema sidebar
+- Add loading indicator for schema sidebar
+- Fix user needing to sign in again after sign up
+- Fix app menu for non-admin users
 
 ## 3.0.0-beta.1
 
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
new file mode 100644
index 00000000..e8461a4a
--- /dev/null
+++ b/CONFIGURATION.md
@@ -0,0 +1,208 @@
+
+_This file was generated by `scripts/generate-configs.js` using `server/lib/config/configItems.js`._
+
+# Configuration
+
+SQLPad may be configured via environment variables, config file, or command line flag.
+
+Config file path may be specified passing command line option `--config` or environment variable SQLPAD_CONFIG.
+For example:
+
+```sh
+sqlpad --config ~/.sqlpadrc
+```
+
+Using a config file or environment variables recommended. For list of command line flags run `sqlpad -h`.
+
+## Version 3 changes
+
+Previously SQLPad supported a default dbPath of `$HOME/sqlpad/db` and a default config file path of `$HOME/.sqlpadrc`. 
+
+These defaults have been removed in version 3.
+
+## Environment Variables
+```sh
+SQLPAD_CONFIG=
+SQLPAD_IP=0.0.0.0
+SQLPAD_PORT=80
+SQLPAD_SYSTEMD_SOCKET=false
+SQLPAD_HTTPS_PORT=443
+SQLPAD_DB_PATH=
+SQLPAD_BASE_URL=
+SQLPAD_PASSPHRASE=At least the sensitive bits won't be plain text?
+CERT_PASSPHRASE=
+KEY_PATH=
+CERT_PATH=
+SQLPAD_ADMIN=
+SQLPAD_DEBUG=false
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+PUBLIC_URL=
+DISABLE_USERPASS_AUTH=false
+SQLPAD_ALLOW_CSV_DOWNLOAD=true
+SQLPAD_EDITOR_WORD_WRAP=false
+SQLPAD_QUERY_RESULT_MAX_ROWS=50000
+SQLPAD_SLACK_WEBHOOK=
+SQLPAD_TABLE_CHART_LINKS_REQUIRE_AUTH=true
+SQLPAD_SMTP_FROM=
+SQLPAD_SMTP_HOST=
+SQLPAD_SMTP_PORT=
+SQLPAD_SMTP_SECURE=true
+SQLPAD_SMTP_USER=
+SQLPAD_SMTP_PASSWORD=
+WHITELISTED_DOMAINS=
+SQLPAD_DISABLE_UPDATE_CHECK=false
+SAML_ENTRY_POINT=
+SAML_ISSUER=
+SAML_CALLBACK_URL=
+SAML_CERT=
+SAML_AUTH_CONTEXT=
+
+```
+
+## INI config
+```ini
+; IP address to bind to. By default SQLPad will listen from all available addresses (0.0.0.0).
+SQLPAD_IP="0.0.0.0"
+
+; Port for SQLPad to listen on.
+SQLPAD_PORT="80"
+
+; Acquire socket from systemd if available
+SQLPAD_SYSTEMD_SOCKET="false"
+
+; Port for SQLPad to listen on.
+SQLPAD_HTTPS_PORT="443"
+
+; Directory to store SQLPad embedded database content. This includes queries, users, query result cache files, etc.
+SQLPAD_DB_PATH=""
+
+; Path to mount sqlpad app following domain. Example, if '/sqlpad' is provided queries page would be mydomain.com/sqlpad/queries
+SQLPAD_BASE_URL=""
+
+; A string of text used to encrypt sensitive values when stored on disk.
+SQLPAD_PASSPHRASE="At least the sensitive bits won't be plain text?"
+
+; Passphrase for your SSL certification file
+CERT_PASSPHRASE=""
+
+; Absolute path to where SSL certificate key is stored
+KEY_PATH=""
+
+; Absolute path to where SSL certificate is stored
+CERT_PATH=""
+
+; Email address to whitelist/give admin permissions to
+SQLPAD_ADMIN=""
+
+; Add a variety of logging to console while running SQLPad
+SQLPAD_DEBUG="false"
+
+; Google Client ID used for OAuth setup. Authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'
+GOOGLE_CLIENT_ID=""
+
+; Google Client Secret used for OAuth setup. Authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'
+GOOGLE_CLIENT_SECRET=""
+
+; Public URL used for OAuth setup and email links. Protocol expected. Example: https://mysqlpad.com
+PUBLIC_URL=""
+
+; Set to TRUE to disable built-in user authentication. Use to restrict auth to OAuth only.
+DISABLE_USERPASS_AUTH="false"
+
+; Enable csv and xlsx downloads.
+SQLPAD_ALLOW_CSV_DOWNLOAD="true"
+
+; Enable word wrapping in SQL editor.
+SQLPAD_EDITOR_WORD_WRAP="false"
+
+; By default query results are limited to 50,000 records.
+SQLPAD_QUERY_RESULT_MAX_ROWS="50000"
+
+; Supply incoming Slack webhook URL to post query when saved.
+SQLPAD_SLACK_WEBHOOK=""
+
+; When false, table and chart result links will be operational without login.
+SQLPAD_TABLE_CHART_LINKS_REQUIRE_AUTH="true"
+
+; From email address for SMTP. Required in order to send invitation emails.
+SQLPAD_SMTP_FROM=""
+
+; Host address for SMTP. Required in order to send invitation emails.
+SQLPAD_SMTP_HOST=""
+
+; Port for SMTP. Required in order to send invitation emails.
+SQLPAD_SMTP_PORT=""
+
+; Toggle to use secure connection when using SMTP.
+SQLPAD_SMTP_SECURE="true"
+
+; Username for SMTP. Required in order to send invitation emails.
+SQLPAD_SMTP_USER=""
+
+; Password for SMTP.
+SQLPAD_SMTP_PASSWORD=""
+
+; Allows pre-approval of email domains. Delimit multiple domains by empty space.
+WHITELISTED_DOMAINS=""
+
+; If disabled, SQLPad will no longer poll npmjs.com to see if an update is available.
+SQLPAD_DISABLE_UPDATE_CHECK="false"
+
+; SAML Entry point URL
+SAML_ENTRY_POINT=""
+
+; SAML Issuer
+SAML_ISSUER=""
+
+; SAML callback URL
+SAML_CALLBACK_URL=""
+
+; SAML certificate in Base64
+SAML_CERT=""
+
+; SAML authentication context URL
+SAML_AUTH_CONTEXT=""
+
+
+```
+
+## JSON config
+```json
+{
+  "ip": "0.0.0.0",
+  "port": 80,
+  "systemdSocket": false,
+  "httpsPort": 443,
+  "dbPath": "",
+  "baseUrl": "",
+  "passphrase": "At least the sensitive bits won't be plain text?",
+  "certPassphrase": "",
+  "keyPath": "",
+  "certPath": "",
+  "admin": "",
+  "debug": false,
+  "googleClientId": "",
+  "googleClientSecret": "",
+  "publicUrl": "",
+  "disableUserpassAuth": false,
+  "allowCsvDownload": true,
+  "editorWordWrap": false,
+  "queryResultMaxRows": 50000,
+  "slackWebhook": "",
+  "tableChartLinksRequireAuth": true,
+  "smtpFrom": "",
+  "smtpHost": "",
+  "smtpPort": "",
+  "smtpSecure": true,
+  "smtpUser": "",
+  "smtpPassword": "",
+  "whitelistedDomains": "",
+  "disableUpdateCheck": false,
+  "samlEntryPoint": "",
+  "samlIssuer": "",
+  "samlCallbackUrl": "",
+  "samlCert": "",
+  "samlAuthContext": ""
+}
+```
diff --git a/scripts/generate-configs.js b/scripts/generate-configs.js
new file mode 100644
index 00000000..198dea81
--- /dev/null
+++ b/scripts/generate-configs.js
@@ -0,0 +1,61 @@
+const fs = require('fs');
+const path = require('path');
+const configItems = require('../server/lib/config/configItems')
+
+let env = '';
+let json = {};
+let ini = '';
+
+configItems.forEach(item => {
+  env += `${item.envVar}=${item.default}\n`
+  
+  if (item.key !== 'config') {
+    json[item.key] = item.default;
+
+    if (item.description) {
+      ini += `; ${item.description}\n${item.envVar}="${item.default}"\n\n`
+    } else {
+      ini += `${item.envVar}="${item.default}"\n`
+    }
+  }
+})
+
+const markdown = `
+_This file was generated by \`scripts/generate-configs.js\` using \`server/lib/config/configItems.js\`._
+
+# Configuration
+
+SQLPad may be configured via environment variables, config file, or command line flag.
+
+Config file path may be specified passing command line option \`--config\` or environment variable SQLPAD_CONFIG.
+For example:
+
+\`\`\`sh
+sqlpad --config ~/.sqlpadrc
+\`\`\`
+
+Using a config file or environment variables recommended. For list of command line flags run \`sqlpad -h\`.
+
+## Version 3 changes
+
+Previously SQLPad supported a default dbPath of \`$HOME/sqlpad/db\` and a default config file path of \`$HOME/.sqlpadrc\`. 
+
+These defaults have been removed in version 3.
+
+## Environment Variables
+\`\`\`sh
+${env}
+\`\`\`
+
+## INI config
+\`\`\`ini
+${ini}
+\`\`\`
+
+## JSON config
+\`\`\`json
+${JSON.stringify(json, null, 2)}
+\`\`\`
+`
+
+fs.writeFileSync(path.join(__dirname, '../CONFIGURATION.md'), markdown, { encoding: 'utf8'})
\ No newline at end of file
diff --git a/server/lib/cli-flow.js b/server/lib/cli-flow.js
index df7fde74..849943ff 100644
--- a/server/lib/cli-flow.js
+++ b/server/lib/cli-flow.js
@@ -1,53 +1,37 @@
-const fs = require('fs');
-const path = require('path');
 const minimist = require('minimist');
 const argv = minimist(process.argv.slice(2));
 const packageJson = require('../package.json');
-const userHome =
-  process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
-const savedCliFilePath = path.join(userHome, '.sqlpadrc');
-
-const helpText = `
-
-SQLPad Help:
-
-Usage: sqlpad [options]
-
-Options: 
-
-  --passphrase [phrase]   Passphrase for modest encryption
-                            optional, default: *******
-                            environment var: SQLPAD_PASSPHRASE
-  --dir [path]            Data directory 
-                            optional, default: $HOME/sqlpad/db
-                            environment var: SQLPAD_DB_PATH
-  --ip [ip]               IP address to bind to
-                            optional, default: 0.0.0.0 (all IPs)
-                            environment var: SQLPAD_IP
-  --port [port]           Port to run on 
-                            optional, default: 80
-                            environment var: SQLPAD_PORT
-  --base-url [path]       Base url to mount sqlpad routes to 
-                            optional, default: ''
-                            environment var: SQLPAD_BASE_URL
-  --admin [emailaddress]  Whitelist/add admin permission to email provided.
-                            optional, default: ''
-                            environment var: SQLPAD_ADMIN
-  --debug                 Enable extra console logging
-                            optional, default: false
-                            environment var: SQLPAD_DEBUG (set to TRUE)
+const configItems = require('./config/configItems');
+
+const helpOptions = configItems.map(item => {
+  let lastFlag = '';
+  if (Array.isArray(item.cliFlag)) {
+    lastFlag = item.cliFlag[item.cliFlag.length - 1];
+  } else if (item.cliFlag) {
+    lastFlag = item.cliFlag;
+  }
 
-  --save                  Saves above parameters to file for future use.
-  --forget                Forget parameters previously saved.
+  return {
+    key: item.key,
+    flag: lastFlag,
+    description: item.description,
+    envVar: item.envVar
+  };
+});
 
-  See configuration management page in-application for 
-  additional settings and further documentation.
+const helpText = `
+SQLPad version:  ${packageJson.version}
 
-Example: 
+Usage:  sqlpad [options]
 
-  sqlpad --dir ./sqlpaddata --ip 127.0.0.1 --port 3000 --passphrase secr3t
+Options:
 
-`;
+${helpOptions
+  .filter(option => Boolean(option.flag))
+  .map(option => {
+    return `  --${option.flag}    ${option.description}\n`;
+  })
+  .join('\n')}`;
 
 // If version is requested show version then exit
 if (argv.v || argv.version) {
@@ -60,25 +44,3 @@ if (argv.h || argv.help) {
   console.log(helpText);
   process.exit();
 }
-
-// if --save was passed in via cli, we should save the cli args
-// this file is a simple key/value object where key is the config item key
-if (argv.save) {
-  console.log('Saving your configuration.');
-  console.log("Next time just run 'sqlpad' and this config will be loaded.");
-  fs.writeFileSync(savedCliFilePath, JSON.stringify(argv, null, 2));
-}
-
-// if --forget was passed in via cli we should remove the saved cli args file
-if (argv.forget) {
-  if (fs.existsSync(savedCliFilePath)) {
-    fs.unlinkSync(savedCliFilePath);
-    console.log('Previous configuration removed.');
-  } else {
-    console.log(
-      'No previous configuration saved. Maybe it was a different user?'
-    );
-  }
-  console.log('Exiting...');
-  process.exit();
-}
diff --git a/server/lib/config/configItems.js b/server/lib/config/configItems.js
index e93eaf59..83f75731 100644
--- a/server/lib/config/configItems.js
+++ b/server/lib/config/configItems.js
@@ -1,4 +1,11 @@
 const configItems = [
+  {
+    key: 'config',
+    cliFlag: 'config',
+    envVar: 'SQLPAD_CONFIG',
+    default: '',
+    description: 'JSON/INI file to read for config'
+  },
   {
     key: 'ip',
     cliFlag: 'ip',
@@ -32,7 +39,7 @@ const configItems = [
     key: 'dbPath',
     cliFlag: ['db', 'dbPath', 'dir'],
     envVar: 'SQLPAD_DB_PATH',
-    default: '$HOME/sqlpad/db',
+    default: '',
     description:
       'Directory to store SQLPad embedded database content. This includes queries, users, query result cache files, etc.'
   },
@@ -42,7 +49,7 @@ const configItems = [
     envVar: 'SQLPAD_BASE_URL',
     default: '',
     description:
-      "Path to mount sqlpad app following domain. \nFor example, if '/sqlpad' is provided, queries page \nwould be located at mydomain.com/sqlpad/queries instead of mydomain.com/queries. \nUseful when subdomain is not an option."
+      "Path to mount sqlpad app following domain. Example, if '/sqlpad' is provided queries page would be mydomain.com/sqlpad/queries"
   },
   {
     key: 'passphrase',
@@ -56,7 +63,7 @@ const configItems = [
     key: 'certPassphrase',
     cliFlag: 'cert-passphrase',
     envVar: 'CERT_PASSPHRASE',
-    default: 'No cert',
+    default: '',
     description: 'Passphrase for your SSL certification file'
   },
   {
@@ -78,8 +85,7 @@ const configItems = [
     cliFlag: 'admin',
     envVar: 'SQLPAD_ADMIN',
     default: '',
-    description:
-      'Email address to whitelist/give admin permissions to via command line or environment variable. Useful to preset Admin account or to reinstate admin access without access to the UI.'
+    description: 'Email address to whitelist/give admin permissions to'
   },
   {
     key: 'debug',
@@ -92,14 +98,14 @@ const configItems = [
     key: 'googleClientId',
     envVar: 'GOOGLE_CLIENT_ID',
     description:
-      "Google Client ID used for OAuth setup. Note: authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'",
+      "Google Client ID used for OAuth setup. Authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'",
     default: ''
   },
   {
     key: 'googleClientSecret',
     envVar: 'GOOGLE_CLIENT_SECRET',
     description:
-      "Google Client Secret used for OAuth setup. Note: authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'",
+      "Google Client Secret used for OAuth setup. Authorized redirect URI for sqlpad is '[baseurl]/auth/google/callback'",
     default: ''
   },
   {
@@ -107,55 +113,47 @@ const configItems = [
     envVar: 'PUBLIC_URL',
     cliFlag: 'public-url',
     description:
-      'Public URL used for OAuth setup and links in email communications. Protocol is expected to be provided. Example: https://mysqlpad.com',
+      'Public URL used for OAuth setup and email links. Protocol expected. Example: https://mysqlpad.com',
     default: ''
   },
   {
     key: 'disableUserpassAuth',
     envVar: 'DISABLE_USERPASS_AUTH',
     description:
-      'Set to TRUE to disable built-in user authentication. Useful to restrict authentication to OAuth only.',
+      'Set to TRUE to disable built-in user authentication. Use to restrict auth to OAuth only.',
     default: false
   },
   {
     key: 'allowCsvDownload',
-    label: 'Allow CSV/XLSX Download',
-    description: 'Set to false to disable csv or xlsx downloads.',
+    envVar: 'SQLPAD_ALLOW_CSV_DOWNLOAD',
+    description: 'Enable csv and xlsx downloads.',
     options: [true, false],
     default: true
   },
   {
     key: 'editorWordWrap',
-    label: 'Editor Word Wrap',
-    description: 'Set to true to enable word wrapping in SQL editor.',
+    envVar: 'SQLPAD_EDITOR_WORD_WRAP',
+    description: 'Enable word wrapping in SQL editor.',
     options: [true, false],
     default: false
   },
   {
     key: 'queryResultMaxRows',
-    label: 'Query Result Max Rows',
+    envVar: 'SQLPAD_QUERY_RESULT_MAX_ROWS',
     description: 'By default query results are limited to 50,000 records.',
     default: 50000
   },
   {
     key: 'slackWebhook',
-    label: 'Slack Webhook URL',
+    envVar: 'SQLPAD_SLACK_WEBHOOK',
     description: 'Supply incoming Slack webhook URL to post query when saved.',
     default: ''
   },
-  {
-    key: 'showSchemaCopyButton',
-    label: 'Show Schema Copy Button',
-    description:
-      "Enable a button to copy an object's full schema path in schema explorer. Useful for databases that require fully qualified names.",
-    options: [true, false],
-    default: false
-  },
   {
     key: 'tableChartLinksRequireAuth',
-    label: 'Require Login for Table/Chart Links',
+    envVar: 'SQLPAD_TABLE_CHART_LINKS_REQUIRE_AUTH',
     description:
-      'If set to false, table and chart result links will be operational without having to log in. (These links only execute saved SQL queries, and do not open an endpoint to execute raw SQL.)',
+      'When false, table and chart result links will be operational without login.',
     options: [true, false],
     default: true
   },
@@ -163,7 +161,6 @@ const configItems = [
     key: 'smtpFrom',
     envVar: 'SQLPAD_SMTP_FROM',
     cliFlag: 'smtp-from',
-    label: 'SMTP From',
     description:
       'From email address for SMTP. Required in order to send invitation emails.',
     default: ''
@@ -172,7 +169,6 @@ const configItems = [
     key: 'smtpHost',
     envVar: 'SQLPAD_SMTP_HOST',
     cliFlag: 'smtp-host',
-    label: 'SMTP Host',
     description:
       'Host address for SMTP. Required in order to send invitation emails.',
     default: ''
@@ -181,7 +177,6 @@ const configItems = [
     key: 'smtpPort',
     envVar: 'SQLPAD_SMTP_PORT',
     cliFlag: 'smtp-port',
-    label: 'SMTP Port',
     description: 'Port for SMTP. Required in order to send invitation emails.',
     default: ''
   },
@@ -189,7 +184,6 @@ const configItems = [
     key: 'smtpSecure',
     envVar: 'SQLPAD_SMTP_SECURE',
     cliFlag: 'smtp-secure',
-    label: 'SMTP Use SSL',
     options: [true, false],
     description: 'Toggle to use secure connection when using SMTP.',
     default: true
@@ -198,7 +192,6 @@ const configItems = [
     key: 'smtpUser',
     envVar: 'SQLPAD_SMTP_USER',
     cliFlag: 'smtp-user',
-    label: 'SMTP User',
     description:
       'Username for SMTP. Required in order to send invitation emails.',
     default: ''
@@ -207,31 +200,27 @@ const configItems = [
     key: 'smtpPassword',
     envVar: 'SQLPAD_SMTP_PASSWORD',
     cliFlag: 'smtp-password',
-    label: 'SMTP Password',
     description: 'Password for SMTP.',
     default: ''
   },
   {
     key: 'whitelistedDomains',
-    label: 'Whitelisted Domains',
     envVar: 'WHITELISTED_DOMAINS',
     cliFlag: 'whitelisted-domains',
     description:
-      "Allows whitelisting of email domains so individual email addresses do not need to be whitelisted. Domains must be delimited by whitespace. For example, 'baz.com foo.bar.com' will whitelist sara@baz.com and john@foo.bar.com",
+      'Allows pre-approval of email domains. Delimit multiple domains by empty space.',
     default: ''
   },
   {
     key: 'disableUpdateCheck',
     envVar: 'SQLPAD_DISABLE_UPDATE_CHECK',
     cliFlag: 'disable-update-check',
-    label: 'Disable update check',
     options: [true, false],
     description:
       'If disabled, SQLPad will no longer poll npmjs.com to see if an update is available.',
     default: false
   },
   {
-    interface: 'env',
     key: 'samlEntryPoint',
     envVar: 'SAML_ENTRY_POINT',
     cliFlag: 'saml-entry-point',
@@ -239,7 +228,6 @@ const configItems = [
     default: ''
   },
   {
-    interface: 'env',
     key: 'samlIssuer',
     envVar: 'SAML_ISSUER',
     cliFlag: 'saml-issuer',
@@ -247,7 +235,6 @@ const configItems = [
     default: ''
   },
   {
-    interface: 'env',
     key: 'samlCallbackUrl',
     envVar: 'SAML_CALLBACK_URL',
     cliFlag: 'saml-callback-url',
@@ -255,7 +242,6 @@ const configItems = [
     default: ''
   },
   {
-    interface: 'env',
     key: 'samlCert',
     envVar: 'SAML_CERT',
     cliFlag: 'saml-cert',
diff --git a/server/lib/config/fromDefault.js b/server/lib/config/fromDefault.js
index ce86baa5..2d162c4a 100644
--- a/server/lib/config/fromDefault.js
+++ b/server/lib/config/fromDefault.js
@@ -1,25 +1,14 @@
-const path = require('path');
 const definitions = require('./configItems');
 
 /**
  * Gets default config values
- * dbPath gets a default based on USERPROFILE/HOME
  * @returns {object} configMap
  */
 module.exports = function getDefaultConfig() {
   const defaultMap = {};
 
   definitions.forEach(definition => {
-    if (definition.key === 'dbPath') {
-      const userHome =
-        process.platform === 'win32'
-          ? process.env.USERPROFILE
-          : process.env.HOME;
-      const defaultDbPath = path.join(userHome, 'sqlpad/db');
-      defaultMap.dbPath = defaultDbPath;
-    } else if (definition.hasOwnProperty('default')) {
-      defaultMap[definition.key] = definition.default;
-    }
+    defaultMap[definition.key] = definition.default;
   });
 
   return defaultMap;
diff --git a/server/lib/config/fromFile.js b/server/lib/config/fromFile.js
new file mode 100644
index 00000000..16ab2939
--- /dev/null
+++ b/server/lib/config/fromFile.js
@@ -0,0 +1,56 @@
+const fs = require('fs');
+const ini = require('ini');
+const configItems = require('./configItems');
+
+/**
+ * Reads and parses config file.
+ * This file may be either JSON or INI file.
+ * @param {string} configFilePath
+ */
+function fromFile(configFilePath) {
+  let parsedFile = {};
+  const warnings = [];
+
+  if (fs.existsSync(configFilePath)) {
+    const fileText = fs.readFileSync(configFilePath, { encoding: 'utf8' });
+
+    try {
+      parsedFile = JSON.parse(fileText);
+    } catch (error) {
+      try {
+        parsedFile = ini.parse(fileText);
+      } catch (error) {
+        throw new Error(`Error parsing config file ${configFilePath}`);
+      }
+    }
+
+    // Validate that the config file uses the keys defined in configItems
+    // Prior to SQLPad 3 the saved config file was a JSON result of what minimist parsed from argv
+    // This means that there could be cliFlag's in the json ie `cert-passphrase` or `dir` for dbPath
+    // These are no longer supported from a file
+    Object.keys(parsedFile).forEach(key => {
+      const configItem = configItems.find(item => item.key === key);
+      if (!configItem) {
+        let warningMessage = `Config key ${key} in file ${configFilePath} not recognized.`;
+
+        // Find the item it might be and give the user a hint
+        const maybeItem = configItems.find(item => {
+          if (Array.isArray(item.cliFlag)) {
+            return item.cliFlag.includes(key);
+          }
+          return item.cliFlag === key;
+        });
+        if (maybeItem) {
+          warningMessage += ` Did you mean ${maybeItem.key}?`;
+        } else {
+          warningMessage += ' It can likely be removed.';
+        }
+        warnings.push(warningMessage);
+      }
+    });
+  }
+
+  return [parsedFile, warnings];
+}
+
+module.exports = fromFile;
diff --git a/server/lib/config/getOldConfigWarning.js b/server/lib/config/getOldConfigWarning.js
new file mode 100644
index 00000000..6d946126
--- /dev/null
+++ b/server/lib/config/getOldConfigWarning.js
@@ -0,0 +1,24 @@
+const path = require('path');
+const fs = require('fs');
+
+const userHome =
+  process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
+const oldConfigFilePath = path.join(userHome, '.sqlpadrc');
+const oldDefaultDbPath = path.join(userHome, 'sqlpad/db');
+
+let message = `
+  "dbPath" not provided in config. 
+  This was previously defaulted to ${oldDefaultDbPath}.
+  This must now be explicitly set.`;
+
+if (fs.existsSync(oldConfigFilePath)) {
+  message += `
+  This value may have been provided by old saved config file at ${oldConfigFilePath}. 
+  Config file location must also now be provided explicitly.
+  Use --config at command line or set environment variable SQLPAD_CONFIG
+  `;
+}
+
+module.exports = function getOldConfigWarning() {
+  return message;
+};
diff --git a/server/lib/config/index.js b/server/lib/config/index.js
index a99f27c6..f6dcb2a3 100644
--- a/server/lib/config/index.js
+++ b/server/lib/config/index.js
@@ -1,33 +1,36 @@
-const fs = require('fs');
-const path = require('path');
 const minimist = require('minimist');
 const fromDefault = require('./fromDefault');
 const fromEnv = require('./fromEnv');
 const fromCli = require('./fromCli');
+const fromFile = require('./fromFile');
+const getOldConfigWarning = require('./getOldConfigWarning');
 
-// argv
 const argv = minimist(process.argv.slice(2));
-
-// Saved argv
-const userHome =
-  process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
-const filePath = path.join(userHome, '.sqlpadrc');
-const savedArgv = fs.existsSync(filePath)
-  ? JSON.parse(fs.readFileSync(filePath, { encoding: 'utf8' }))
-  : {};
+const configFilePath = argv.config || process.env.SQLPAD_CONFIG;
 
 const defaultConfig = fromDefault();
-const cliConfig = fromCli(argv);
-const savedCliConfig = fromCli(savedArgv);
 const envConfig = fromEnv();
+const [fileConfig, warnings] = fromFile(configFilePath);
+const cliConfig = fromCli(argv);
 
-const all = Object.assign(
-  {},
-  defaultConfig,
-  envConfig,
-  savedCliConfig,
-  cliConfig
-);
+// Old files might have some values no longer recognized
+if (warnings.length) {
+  warnings.forEach(warning => console.warn(warning));
+}
+
+const all = Object.assign({}, defaultConfig, envConfig, fileConfig, cliConfig);
+
+// Clean string boolean values
+Object.keys(all).forEach(key => {
+  const value = all[key];
+  if (typeof value === 'string') {
+    if (value.trim().toLowerCase() === 'true') {
+      all[key] = true;
+    } else if (value.trim().toLowerCase() === 'false') {
+      all[key] = false;
+    }
+  }
+});
 
 exports.get = function get(key) {
   if (!key) {
@@ -37,6 +40,12 @@ exports.get = function get(key) {
   if (!all.hasOwnProperty(key)) {
     throw new Error(`config item ${key} not defined in configItems.js`);
   }
+
+  if (key === 'dbPath' && !all.dbPath) {
+    console.error(getOldConfigWarning());
+    throw new Error(`dbPath not defined`);
+  }
+
   return all[key];
 };
 
diff --git a/server/package.json b/server/package.json
index fc5296cd..8324989e 100644
--- a/server/package.json
+++ b/server/package.json
@@ -43,6 +43,7 @@
     "express-session": "^1.16.2",
     "hdb": "^0.15.4",
     "helmet": "^3.18.0",
+    "ini": "^1.3.5",
     "joi": "^12.0.0",
     "json2csv": "^3.11.5",
     "latest-version": "^3.1.0",
diff --git a/server/routes/app.js b/server/routes/app.js
index 2f9e4854..5d33bbb1 100644
--- a/server/routes/app.js
+++ b/server/routes/app.js
@@ -26,7 +26,6 @@ router.get('*/api/app', async (req, res) => {
         publicUrl: config.get('publicUrl'),
         allowCsvDownload: config.get('allowCsvDownload'),
         editorWordWrap: config.get('editorWordWrap'),
-        showSchemaCopyButton: config.get('showSchemaCopyButton'),
         baseUrl: config.get('baseUrl'),
         smtpConfigured: config.smtpConfigured(),
         googleAuthConfigured: config.googleAuthConfigured(),


Test patch:
diff --git a/server/test/api/app.js b/server/test/api/app.js
index 4c9d2947..aca838fd 100644
--- a/server/test/api/app.js
+++ b/server/test/api/app.js
@@ -12,7 +12,6 @@ const expectedConfigKeys = [
   'baseUrl',
   'allowCsvDownload',
   'editorWordWrap',
-  'showSchemaCopyButton',
   'publicUrl',
   'smtpConfigured',
   'googleAuthConfigured',
diff --git a/server/test/fixtures/config.ini b/server/test/fixtures/config.ini
new file mode 100644
index 00000000..36695d3b
--- /dev/null
+++ b/server/test/fixtures/config.ini
@@ -0,0 +1,4 @@
+; this is a comment
+dbPath = 'dbPath'
+baseUrl = 'baseUrl'
+certPassphrase = 'certPassphrase'
\ No newline at end of file
diff --git a/server/test/fixtures/config.json b/server/test/fixtures/config.json
new file mode 100644
index 00000000..6cf6cab5
--- /dev/null
+++ b/server/test/fixtures/config.json
@@ -0,0 +1,5 @@
+{
+  "dbPath": "dbPath",
+  "baseUrl": "baseUrl",
+  "certPassphrase": "certPassphrase"
+}
diff --git a/server/test/fixtures/oldConfig.json b/server/test/fixtures/oldConfig.json
new file mode 100644
index 00000000..ef226915
--- /dev/null
+++ b/server/test/fixtures/oldConfig.json
@@ -0,0 +1,5 @@
+{
+  "dbPath": "dbPath",
+  "baseUrl": "baseUrl",
+  "cert-passphrase": "cert-passphrase"
+}
diff --git a/server/test/lib/config.js b/server/test/lib/config.js
index 075e7c99..80174e0c 100644
--- a/server/test/lib/config.js
+++ b/server/test/lib/config.js
@@ -30,15 +30,6 @@ describe('config', function() {
 });
 
 describe('lib/config', function() {
-  // TODO test when control is inverted/dependencies injected
-  // set any process.env variables or args here
-  // process.argv.push('--debug')
-  // process.env.SQLPAD_DEBUG = 'FALSE'
-  // process.env.GOOGLE_CLIENT_ID = 'google-client-id'
-
-  // TODO current config helper test will pick up saved sqlpad config on system if it exists
-  // This makes testing difficult when a sqlpad configuration is saved.
-  // Loading a config should likely be explicit
   it('.get() should get a value provided by default', async function() {
     assert.equal(config.get('httpsPort'), 443, 'httpsPort=443');
   });
diff --git a/server/test/lib/configFromFile.js b/server/test/lib/configFromFile.js
new file mode 100644
index 00000000..35f0055e
--- /dev/null
+++ b/server/test/lib/configFromFile.js
@@ -0,0 +1,34 @@
+const assert = require('assert');
+const path = require('path');
+const fromFile = require('../../lib/config/fromFile');
+
+describe('configFromFile', function() {
+  it('handles missing file', function() {
+    const [config] = fromFile(path.join(__dirname, '/missing.ini'));
+    assert.equal(Object.keys(config).length, 0, 'empty object');
+  });
+
+  it('reads INI', function() {
+    const [config] = fromFile(path.join(__dirname, '../fixtures/config.ini'));
+    assert.equal(config.dbPath, 'dbPath', 'dbPath');
+    assert.equal(config.baseUrl, 'baseUrl', 'baseUrl');
+    assert.equal(config.certPassphrase, 'certPassphrase', 'certPassphrase');
+    assert.equal(Object.keys(config).length, 3, '3 items');
+  });
+
+  it('reads JSON', function() {
+    const [config] = fromFile(path.join(__dirname, '../fixtures/config.json'));
+    assert.equal(config.dbPath, 'dbPath', 'dbPath');
+    assert.equal(config.baseUrl, 'baseUrl', 'baseUrl');
+    assert.equal(config.certPassphrase, 'certPassphrase', 'certPassphrase');
+    assert.equal(Object.keys(config).length, 3, '3 items');
+  });
+
+  it('Warns for old JSON', function() {
+    const [config, warnings] = fromFile(
+      path.join(__dirname, '../fixtures/oldConfig.json')
+    );
+    assert(config);
+    assert.equal(warnings.length, 1, 'has warnings');
+  });
+});