diff --git a/docs/guides/local-https-setup.md b/docs/guides/local-https-setup.md
new file mode 100644
index 000000000..378b4c15f
--- /dev/null
+++ b/docs/guides/local-https-setup.md
@@ -0,0 +1,142 @@
+# Localhost HTTPS Setup
+
+These instructions are for MacOS, verified on macOS Mojave version 10.14.6
+
+## SSL Key and Certificate
+
+1. Generate SSL key and cert
+
+Copy and paste these commands in the terminal to run them.
+
+> Make sure to change the hostname `dev.mydomain.com` in both places to your desired value.
+
+```bash
+openssl req -new -x509 -nodes -sha256 -days 3650 \
+ -newkey rsa:2048 -out dev-proxy.crt -keyout dev-proxy.key \
+ -extensions SAN -reqexts SAN -subj /CN=dev.mydomain.com \
+ -config <(cat /etc/ssl/openssl.cnf \
+    <(printf '[ext]\nbasicConstraints=critical,CA:TRUE,pathlen:0\n') \
+    <(printf '[SAN]\nsubjectAltName=DNS:dev.mydomain.com,IP:127.0.0.1\n'))
+```
+
+2. Add cert to your system keychain
+
+```bash
+sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain dev-proxy.crt
+```
+
+3.  Put the files `dev-proxy.key` and `dev-proxy.crt` in your app's dir.
+
+    > Alternatively, you can put them in one of the directories listed below:
+
+    - `src`
+    - `test`
+    - `config`
+
+## Development in HTTPS
+
+After everything's setup, you can start development in HTTPS with the following steps:
+
+1.  Using your favorite editor, add this line to your `/etc/hosts` file.
+
+    > Change the hostname `dev.mydomain.com` accordingly if you used a different one.
+
+```
+127.0.0.1       dev.mydomain.com
+```
+
+2.  Now to run app dev in HTTPS, set the env `ELECTRODE_DEV_HTTPS` to `8443` and `HOST` to the domain name you created your cert for.
+
+    - Example: `HOST=dev.mydomain.com ELECTRODE_DEV_HTTPS=8443 npm run dev`
+
+    - And point your browser to `https://dev.mydomain.com:8443`
+
+    - If you have access to listen on the standard HTTPS port `443`, then you can set it to `443` or `true`, and use the URL `https://dev.mydomain.com` directly.
+
+    - Another way to trigger HTTPS is with the env `PORT`. If that is set to `443` exactly, then the dev proxy will enter HTTPS mode even if env `ELECTRODE_DEV_HTTPS` is not set.
+
+## Elevated Privilege
+
+Generally, normal users can't run program to listen on network port below 1024.
+
+> but that seems to have changed for MacOS Mojave https://news.ycombinator.com/item?id=18302380
+
+So if you want to set the dev proxy to listen on the standard HTTP port 80 or HTTPS port 443, you might need to give it elevated access.
+
+The recommended approach to achieve this is to run the dev proxy in a separate terminal with elevated access:
+
+```bash
+sudo HOST=dev.mydomain.com PORT=443 npx clap dev-proxy
+```
+
+And then start normal development in another terminal:
+
+```bash
+HOST=dev.mydomain.com npm run dev
+```
+
+### Automatic Elevating (optional)
+
+Optional: for best result, please use the manual approach recommended above.
+
+If your machine requires elevated access for the proxy to listen at a port, then a dialog will pop up to ask you for your password. This is achieved with the module https://www.npmjs.com/package/sudo-prompt
+
+This requirement is automatically detected, but if you want to explicitly trigger the elevated access, you can set the env `ELECTRODE_DEV_ELEVATED` to `true`.
+
+> However, due to restrictions with acquiring elevated access, this automatic acquisition has quirks. For example, the logs from the dev proxy can't be shown in your console.
+
+## Custom Proxy Rules
+
+The dev proxy is using a slightly modified version of [redbird] with some fixes and enhancements that are pending PR merge.
+
+You can provide your own proxy rules with a file `dev-proxy-rules.js` in one of these directories:
+
+- `src`
+- `test`
+- `config`
+
+The file should export a function `setupRules`, like the example below:
+
+```js
+export function setupRules(proxy, options) {
+  const { host, port, protocol } = options;
+  proxy.register(
+    `${protocol}://${host}:${port}/myapi/foo-api`,
+    `https://api.myserver.com/myapi/foo-api`
+  );
+}
+```
+
+Where:
+
+- `proxy` - the redbird proxy instance.
+- `options` - all configuration for the proxy:
+
+  - `host` - hostname proxy is using.
+  - `port` - primary port proxy is listening on.
+  - `appPort` - app server's port the proxy forward to.
+  - `httpPort` - HTTP port proxy is listening on.
+  - `httpsPort` - Port for HTTPS if it is enabled.
+  - `https` - `true`/`false` to indicate if proxy is running in HTTPS mode.
+  - `webpackDev` - `true`/`false` to indicate if running with webpack dev.
+  - `webpackDevPort` - webpack dev server's port the proxy is forwarding to.
+  - `webpackDevPort` - webpack dev server's host the proxy is forwarding to.
+  - `protocol` - primary protocol: `"http"` or `"https"`.
+  - `elevated` - `true`/`false` to indicate if proxy should acquire elevate access.
+
+- The primary protocol is `https` if HTTPS is enabled, else it's `http`.
+
+- `appPort` is the port your app server is expected to listen on. It's determined as follows:
+
+1. env `APP_SERVER_PORT` or `APP_PORT_FOR_PROXY` if it's a valid number.
+2. fallback to `3100`.
+
+- Even if HTTPS is enabled, the proxy always listens on HTTP also. In that case, `httpPort` is determined as follows:
+
+1. env `PORT` if it's defined
+2. if `appPort` is not `3000`, then fallback to `3000`.
+3. finally fallback to `3300`.
+
+The primary API to register your proxy rule is [`proxy.register`](https://www.npmjs.com/package/redbird#redbirdregistersrc-target-opts).
+
+[redbird]: https://www.npmjs.com/package/redbird
diff --git a/packages/subapp-server/lib/setup-hapi-routes.js b/packages/subapp-server/lib/setup-hapi-routes.js
index bc3636332..f6f8440ff 100644
--- a/packages/subapp-server/lib/setup-hapi-routes.js
+++ b/packages/subapp-server/lib/setup-hapi-routes.js
@@ -8,7 +8,6 @@ const _ = require("lodash");
 const Fs = require("fs");
 const Path = require("path");
 const assert = require("assert");
-const Url = require("url");
 const util = require("util");
 const optionalRequire = require("optional-require")(require);
 const scanDir = require("filter-scan-dir");
@@ -139,11 +138,9 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
     return h.continue;
   });
 
-  topOpts.devBundleBase = Url.format({
-    protocol: topOpts.devServer.https ? "https" : "http",
-    hostname: topOpts.devServer.host,
-    port: topOpts.devServer.port,
-    pathname: "/js/"
+  topOpts.devBundleBase = subAppUtil.formUrl({
+    ..._.pick(topOpts.devServer, ["protocol", "host", "port"]),
+    path: "/js/"
   });
 
   // register routes
@@ -192,7 +189,7 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
             .type("text/html; charset=UTF-8")
             .code(200);
         } else if (HttpStatus.redirect[status]) {
-          return h.redirect(data.path);
+          return h.redirect(data.path).code(status);
         } else if (HttpStatus.displayHtml[status]) {
           return h.response(data.html !== undefined ? data.html : data).code(status);
         } else if (status >= 200 && status < 300) {
@@ -256,11 +253,9 @@ async function setupRoutesFromDir(server, pluginOpts, fromDir) {
     });
   }
 
-  topOpts.devBundleBase = Url.format({
-    protocol: topOpts.devServer.https ? "https" : "http",
-    hostname: topOpts.devServer.host,
-    port: topOpts.devServer.port,
-    pathname: "/js/"
+  topOpts.devBundleBase = subAppUtil.formUrl({
+    ..._.pick(topOpts.devServer, ["protocol", "host", "port"]),
+    path: "/js/"
   });
 
   registerRoutes({ routes, topOpts, server });
diff --git a/packages/subapp-server/lib/utils.js b/packages/subapp-server/lib/utils.js
index 6349be871..3b0ab1b9e 100644
--- a/packages/subapp-server/lib/utils.js
+++ b/packages/subapp-server/lib/utils.js
@@ -4,6 +4,13 @@
 
 const Fs = require("fs");
 const Path = require("path");
+const optionalRequire = require("optional-require")(require);
+const {
+  settings = {},
+  devServer = {},
+  fullDevServer = {},
+  httpDevServer = {}
+} = optionalRequire("@xarc/app-dev/config/dev-proxy", { default: {} });
 
 /**
  * Tries to import bundle chunk selector function if the corresponding option is set in the
@@ -55,33 +62,22 @@ const updateFullTemplate = (baseDir, options) => {
   }
 };
 
-function findEnv(keys, defVal) {
-  const k = [].concat(keys).find(x => x && process.env.hasOwnProperty(x));
-  return k ? process.env[k] : defVal;
-}
-
 function getDefaultRouteOptions() {
-  const isDevProxy = process.env.hasOwnProperty("APP_SERVER_PORT");
-  const webpackDev = process.env.WEBPACK_DEV === "true";
+  const { webpackDev, useDevProxy } = settings;
   // temporary location to write build artifacts in dev mode
   const buildArtifacts = ".etmp";
   return {
     pageTitle: "Untitled Electrode Web Application",
-    //
     webpackDev,
-    isDevProxy,
-    //
-    devServer: {
-      host: findEnv([isDevProxy && "HOST", "WEBPACK_DEV_HOST", "WEBPACK_HOST"], "127.0.0.1"),
-      port: findEnv([isDevProxy && "PORT", "WEBPACK_DEV_PORT"], isDevProxy ? "3000" : "2992"),
-      https: Boolean(process.env.WEBPACK_DEV_HTTPS)
-    },
-    //
+    useDevProxy,
+    devServer,
+    fullDevServer,
+    httpDevServer,
     stats: webpackDev ? `${buildArtifacts}/stats.json` : "dist/server/stats.json",
     iconStats: "dist/server/iconstats.json",
     criticalCSS: "dist/js/critical.css",
     buildArtifacts,
-    prodBundleBase: "/js/",
+    prodBundleBase: "/js",
     devBundleBase: "/js",
     cspNonceValue: undefined,
     templateFile: Path.join(__dirname, "..", "resources", "index-page")
diff --git a/packages/subapp-util/lib/index.js b/packages/subapp-util/lib/index.js
index 64b48a966..1e8156200 100644
--- a/packages/subapp-util/lib/index.js
+++ b/packages/subapp-util/lib/index.js
@@ -2,6 +2,7 @@
 
 /* eslint-disable no-console, no-process-exit, max-params */
 
+const Url = require("url");
 const Path = require("path");
 const assert = require("assert");
 const optionalRequire = require("optional-require")(require);
@@ -289,6 +290,24 @@ function refreshAllSubApps() {
   }
 }
 
+const formUrl = ({ protocol = "http", host = "", port = "", path = "" }) => {
+  let proto = protocol.toString().toLowerCase();
+  let host2 = host;
+
+  if (port) {
+    const sp = port.toString();
+    if (sp === "80") {
+      proto = "http";
+    } else if (sp === "443") {
+      proto = "https";
+    } else if (host) {
+      host2 = `${host}:${port}`;
+    }
+  }
+
+  return Url.format({ protocol: proto, host: host2, pathname: path });
+};
+
 module.exports = {
   es6Require,
   scanSubAppsFromDir,
@@ -300,5 +319,6 @@ module.exports = {
   loadSubAppByName,
   loadSubAppServerByName,
   refreshSubAppByName,
-  refreshAllSubApps
+  refreshAllSubApps,
+  formUrl
 };
diff --git a/packages/subapp-web/lib/load.js b/packages/subapp-web/lib/load.js
index 50bf5d16f..71493a6ac 100644
--- a/packages/subapp-web/lib/load.js
+++ b/packages/subapp-web/lib/load.js
@@ -1,6 +1,6 @@
 "use strict";
 
-/* eslint-disable max-statements, no-console, complexity */
+/* eslint-disable max-statements, no-console, complexity, no-magic-numbers */
 
 /*
  * - Figure out all the dependencies and bundles a subapp needs and make sure
@@ -19,20 +19,34 @@ const retrieveUrl = require("request");
 const util = require("./util");
 const xaa = require("xaa");
 const jsesc = require("jsesc");
-const { loadSubAppByName, loadSubAppServerByName } = require("subapp-util");
+const { loadSubAppByName, loadSubAppServerByName, formUrl } = require("subapp-util");
 
 // global name to store client subapp runtime, ie: window.xarcV1
 // V1: version 1.
 const xarc = "window.xarcV1";
 
 // Size threshold of initial state string to embed it as a application/json script tag
-// It's more efficent to JSON.parse large JSON data instead of embedding them as JS.
+// It's more efficient to JSON.parse large JSON data instead of embedding them as JS.
 // https://quipblog.com/efficiently-loading-inlined-json-data-911960b0ac0a
 // > The data sizes are as follows: large is 1.7MB of JSON, medium is 130K,
 // > small is 10K and tiny is 781 bytes.
 const INITIAL_STATE_SIZE_FOR_JSON = 1024;
 let INITIAL_STATE_TAG_ID = 0;
 
+const makeDevDebugMessage = (msg, reportLink = true) => {
+  const reportMsg = reportLink
+    ? `\nError: Please capture this info and submit a bug report at https://github.com/electrode-io/electrode`
+    : "";
+  return `Error: at ${util.removeCwd(__filename)}
+${msg}${reportMsg}`;
+};
+
+const makeDevDebugHtml = msg => {
+  return `<h1 style="background-color: red">DEV ERROR</h1>
+<p><pre style="color: red">${msg}</pre></p>
+<!-- ${msg} -->`;
+};
+
 module.exports = function setup(setupContext, { props: setupProps }) {
   // TODO: create JSON schema to validate props
 
@@ -61,10 +75,18 @@ module.exports = function setup(setupContext, { props: setupProps }) {
   // to inline in the index page.
   //
   const retrieveDevServerBundle = async () => {
-    return new Promise((resolve, reject) => {
-      retrieveUrl(`${bundleBase}${bundleAsset.name}`, (err, resp, body) => {
-        if (err) {
-          reject(err);
+    return new Promise(resolve => {
+      const routeOptions = setupContext.routeOptions;
+      const path = `${bundleBase}${bundleAsset.name}`;
+      const bundleUrl = formUrl({ ...routeOptions.httpDevServer, path });
+      retrieveUrl(bundleUrl, (err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          const msg = makeDevDebugMessage(
+            `Error: fail to retrieve subapp bundle from '${bundleUrl}' for inlining in index HTML
+${err || body}`
+          );
+          console.error(msg); // eslint-disable-line
+          resolve(makeDevDebugHtml(msg));
         } else {
           resolve(`<script>/*${name}*/${body}</script>`);
         }
@@ -81,7 +103,7 @@ module.exports = function setup(setupContext, { props: setupProps }) {
   let inlineSubAppJs;
 
   const prepareSubAppJsBundle = () => {
-    const webpackDev = process.env.WEBPACK_DEV === "true";
+    const { webpackDev } = setupContext.routeOptions;
 
     if (setupProps.inlineScript === "always" || (setupProps.inlineScript === true && !webpackDev)) {
       if (!webpackDev) {
@@ -93,7 +115,9 @@ module.exports = function setup(setupContext, { props: setupProps }) {
         } else if (ext === ".css") {
           inlineSubAppJs = `<style id="${name}">${src}</style>`;
         } else {
-          inlineSubAppJs = `<!-- UNKNOWN bundle extension ${name} -->`;
+          const msg = makeDevDebugMessage(`Error: UNKNOWN bundle extension ${name}`);
+          console.error(msg); // eslint-disable-line
+          inlineSubAppJs = makeDevDebugHtml(msg);
         }
       } else {
         inlineSubAppJs = true;
@@ -251,12 +275,13 @@ ${dynInitialState}<script>${xarc}.startSubAppOnLoad({
       const handleError = err => {
         if (process.env.NODE_ENV !== "production") {
           const stack = util.removeCwd(err.stack);
-          console.error(`SSR subapp ${name} failed <error>${stack}</error>`); // eslint-disable-line
-          outputSpot.add(`<!-- SSR subapp ${name} failed
-
-${stack}
-
--->`);
+          const msg = makeDevDebugMessage(
+            `Error: SSR subapp ${name} failed
+${stack}`,
+            false // SSR failure is likely an issue in user code, don't show link to report bug
+          );
+          console.error(msg); // eslint-disable-line
+          outputSpot.add(makeDevDebugHtml(msg));
         } else if (request && request.log) {
           request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
         }
diff --git a/packages/xarc-app-dev/.eslintrc b/packages/xarc-app-dev/.eslintrc
deleted file mode 100644
index 8d5eed087..000000000
--- a/packages/xarc-app-dev/.eslintrc
+++ /dev/null
@@ -1,30 +0,0 @@
----
-extends:
-  - walmart/configurations/es6-node
-parserOptions:
-  sourceType: module
-rules:
-  strict:
-    - 'off'
-    - global
-  no-process-exit:
-    - 'off'
-  func-style:
-    - 'off'
-  global-require:
-    - 'off'
-  no-magic-numbers:
-    - 'off'
-  no-console:
-    - 'off'
-  quotes:
-    - error
-    - double
-    - avoidEscape: true
-      allowTemplateLiterals: true
-  max-len:
-    - 2
-    - 120
-  no-extra-parens: [0]
-  prefer-spread:
-    - 'off'
diff --git a/packages/xarc-app-dev/config/archetype.js b/packages/xarc-app-dev/config/archetype.js
index cc4d3ca46..3121f75c3 100644
--- a/packages/xarc-app-dev/config/archetype.js
+++ b/packages/xarc-app-dev/config/archetype.js
@@ -1,124 +1,29 @@
 "use strict";
 
 const Path = require("path");
-const optionalRequire = require("optional-require")(require);
-const userConfig = Object.assign({}, optionalRequire(Path.resolve("archetype/config")));
 const { merge } = require("lodash");
 
 const devPkg = require("../package.json");
 const devDir = Path.join(__dirname, "..");
 const devRequire = require(`../require`);
 const configDir = `${devDir}/config`;
-const xenvConfig = devRequire("xenv-config");
+const xenvConfig = require("xenv-config");
 const _ = require("lodash");
 
-const defaultOptimizeCssOptions = {
-  cssProcessorOptions: {
-    zindex: false
-  }
-};
-const options = (userConfig && userConfig.options) || {};
-
-const webpackConfigSpec = {
-  devHostname: { env: ["WEBPACK_HOST", "WEBPACK_DEV_HOST"], default: "localhost" },
-  devPort: { env: "WEBPACK_DEV_PORT", default: 2992 },
-  // Using a built-in reverse proxy, the webpack dev assets are served from the
-  // same host and port as the app.  In that case, the URLs to assets are relative
-  // without protocol, host, port.
-  // However, user can simulate CDN server with the proxy and have assets URLs
-  // specifying different host/port from the app.  To do that, the following
-  // should be defined.
-  cdnProtocol: { env: ["WEBPACK_DEV_CDN_PROTOCOL"], type: "string", default: null },
-  cdnHostname: { env: ["WEBPACK_DEV_CDN_HOST"], type: "string", default: null },
-  cdnPort: { env: ["WEBPACK_DEV_CDN_PORT"], default: 0 },
-  //
-  // in dev mode, all webpack output are saved to memory only, but some files like
-  // stats.json are needed by different uses and the stats partial saves a copy to
-  // disk.  It will use this as the path to save the file.
-  devArtifactsPath: { env: "WEBPACK_DEV_ARTIFACTS_PATH", default: ".etmp" },
-  cssModuleSupport: { env: "CSS_MODULE_SUPPORT", type: "boolean", default: undefined },
-  enableBabelPolyfill: { env: "ENABLE_BABEL_POLYFILL", default: false },
-  enableNodeSourcePlugin: { env: "ENABLE_NODESOURCE_PLUGIN", default: false },
-  enableHotModuleReload: { env: "WEBPACK_HOT_MODULE_RELOAD", default: true },
-  enableWarningsOverlay: { env: "WEBPACK_DEV_WARNINGS_OVERLAY", default: true },
-  woffFontInlineLimit: { env: "WOFF_FONT_INLINE_LIMIT", default: 1000 },
-  preserveSymlinks: {
-    env: ["WEBPACK_PRESERVE_SYMLINKS", "NODE_PRESERVE_SYMLINKS"],
-    default: false
-  },
-  enableShortenCSSNames: { env: "ENABLE_SHORTEN_CSS_NAMES", default: true },
-  minimizeSubappChunks: { env: "MINIMIZE_SUBAPP_CHUNKS", default: false },
-  optimizeCssOptions: {
-    env: "OPTIMIZE_CSS_OPTIONS",
-    type: "json",
-    default: defaultOptimizeCssOptions
-  },
-  loadDlls: {
-    env: "ELECTRODE_LOAD_DLLS",
-    type: "json",
-    default: {}
-  },
-  minify: {
-    env: "WEBPACK_MINIFY",
-    default: true
-  }
-};
-
-const karmaConfigSpec = {
-  browser: { env: "KARMA_BROWSER", default: "chrome" }
-};
-
-const babelConfigSpec = {
-  enableTypeScript: { env: "ENABLE_BABEL_TYPESCRIPT", default: options.typescript || false },
-  enableDynamicImport: { env: "ENABLE_DYNAMIC_IMPORT", default: false },
-  enableFlow: { env: "ENABLE_BABEL_FLOW", default: true },
-  // require the @flow directive in source to enable FlowJS type stripping
-  flowRequireDirective: { env: "FLOW_REQUIRE_DIRECTIVE", default: false },
-  proposalDecorators: { env: "BABEL_PROPOSAL_DECORATORS", default: false },
-  legacyDecorators: { env: "BABEL_LEGACY_DECORATORS", default: true },
-  transformClassProps: { env: "BABEL_CLASS_PROPS", default: false },
-  looseClassProps: { env: "BABEL_CLASS_PROPS_LOOSE", default: true },
-  envTargets: {
-    env: "BABEL_ENV_TARGETS",
-    type: "json",
-    default: {
-      //`default` and `node` targets object is required
-      default: {
-        ie: "8"
-      },
-      node: process.versions.node.split(".")[0]
-    }
-  },
-  target: {
-    env: "ENV_TARGET",
-    type: "string",
-    default: "default"
-  },
-  // `extendLoader` is used to override `babel-loader` only when `hasMultiTargets=true`
-  extendLoader: {
-    type: "json",
-    default: {}
-  }
-};
+const userConfig = require("./user-config");
 
-const topConfigSpec = {
-  devOpenBrowser: { env: "ELECTRODE_DEV_OPEN_BROWSER", default: false }
-};
-const typeScriptOption =
-  options.typescript === false
-    ? {
-        babel: { enableTypeScript: options.typescript }
-      }
-    : {};
+const webpack = require("./env-webpack");
+const babel = require("./env-babel");
+const karma = require("./env-karma");
 
 const config = {
   devDir,
   devPkg,
   devRequire,
-  webpack: xenvConfig(webpackConfigSpec, userConfig.webpack, { merge }),
-  karma: xenvConfig(karmaConfigSpec, userConfig.karma, { merge }),
+  webpack,
+  karma,
   jest: Object.assign({}, userConfig.jest),
-  babel: xenvConfig(babelConfigSpec, userConfig.babel, { merge }),
+  babel,
   config: Object.assign(
     {},
     {
@@ -133,6 +38,19 @@ const config = {
   )
 };
 
+const topConfigSpec = {
+  devOpenBrowser: { env: "ELECTRODE_DEV_OPEN_BROWSER", default: false }
+};
+
+const { options } = userConfig;
+
+const typeScriptOption =
+  options.typescript === false
+    ? {
+        babel: { enableTypeScript: options.typescript }
+      }
+    : {};
+
 module.exports = Object.assign(
   _.merge(config, typeScriptOption),
   xenvConfig(topConfigSpec, _.pick(userConfig, Object.keys(topConfigSpec)), { merge })
diff --git a/packages/xarc-app-dev/config/dev-proxy.js b/packages/xarc-app-dev/config/dev-proxy.js
new file mode 100644
index 000000000..e3b9709f4
--- /dev/null
+++ b/packages/xarc-app-dev/config/dev-proxy.js
@@ -0,0 +1,90 @@
+"use strict";
+
+const Path = require("path");
+const Fs = require("fs");
+
+const envWebpack = require("./env-webpack");
+const envApp = require("./env-app");
+const envProxy = require("./env-proxy");
+
+function searchSSLCerts() {
+  const searchDirs = ["", "config", "test", "src"];
+  for (const f of searchDirs) {
+    const key = Path.resolve(f, "dev-proxy.key");
+    const cert = Path.resolve(f, "dev-proxy.crt");
+    if (Fs.existsSync(key) && Fs.existsSync(cert)) {
+      return { key, cert };
+    }
+  }
+  return {};
+}
+
+const { host, portForProxy: appPort } = envApp;
+const { webpackDev, devPort: webpackDevPort, devHostname: webpackDevHost } = envWebpack;
+
+let protocol;
+let port;
+let httpPort = envApp.port;
+let { httpsPort } = envProxy;
+const { elevated } = envProxy;
+const useDevProxy = appPort > 0;
+
+if (httpsPort) {
+  port = httpsPort;
+  protocol = "https";
+} else if (httpPort === 443) {
+  port = httpsPort = httpPort;
+  httpPort = appPort !== 3000 ? 3000 : 3300;
+  protocol = "https";
+} else {
+  port = httpPort;
+  protocol = "http";
+}
+
+const settings = {
+  host,
+  port,
+  appPort,
+  httpPort,
+  httpsPort,
+  https: protocol === "https",
+  webpackDev,
+  webpackDevPort,
+  webpackDevHost,
+  protocol,
+  elevated,
+  useDevProxy
+};
+
+const adminPath = `/__proxy_admin`;
+const hmrPath = `/__webpack_hmr`; // this is webpack-hot-middleware's default
+const devPath = `/__electrode_dev`;
+
+const controlPaths = {
+  admin: adminPath,
+  hmr: hmrPath,
+  dev: devPath,
+  status: `${adminPath}/status`,
+  exit: `${adminPath}/exit`,
+  restart: `${adminPath}/restart`,
+  appLog: `${devPath}/log`,
+  reporter: `${devPath}/reporter`
+};
+
+module.exports = {
+  settings,
+  devServer: useDevProxy
+    ? // when using dev proxy, all routes and assets are unified at the same protocol/host/port
+      // so we can just use path to load assets and let browser figure out protocol/host/port
+      // from the location.
+      { protocol: "", host: "", port: "" }
+    : // no dev proxy, so webpack dev server is running at a different port, so need to form
+      // full URL with protocol/host/port to get the assets.
+      { protocol: "http", host: webpackDevHost, port: webpackDevPort, https: false },
+  fullDevServer: { protocol, host, port },
+  // If using dev proxy in HTTPS, then it's also listening on a HTTP port also:
+  httpDevServer: { protocol: "http", host, port: httpPort, https: false },
+  controlPaths,
+  searchSSLCerts,
+  certs: searchSSLCerts()
+};
diff --git a/packages/xarc-app-dev/config/env-app.js b/packages/xarc-app-dev/config/env-app.js
new file mode 100644
index 000000000..69786bc24
--- /dev/null
+++ b/packages/xarc-app-dev/config/env-app.js
@@ -0,0 +1,17 @@
+"use strict";
+
+const xenvConfig = require("xenv-config");
+const { merge } = require("lodash");
+
+const appConfigSpec = {
+  host: { env: ["HOST"], default: "localhost" },
+  port: { env: ["PORT"], default: 3000 },
+  portForProxy: {
+    env: ["APP_PORT_FOR_PROXY", "APP_SERVER_PORT"],
+    default: 0,
+    envMap: { false: 0, true: 3100 },
+    post: x => x || 0
+  }
+};
+
+module.exports = xenvConfig(appConfigSpec, {}, { merge });
diff --git a/packages/xarc-app-dev/config/env-babel.js b/packages/xarc-app-dev/config/env-babel.js
new file mode 100644
index 000000000..c56d09a05
--- /dev/null
+++ b/packages/xarc-app-dev/config/env-babel.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const xenvConfig = require("xenv-config");
+const { merge } = require("lodash");
+
+const userConfig = require("./user-config");
+const { options } = userConfig;
+
+const babelConfigSpec = {
+  enableTypeScript: { env: "ENABLE_BABEL_TYPESCRIPT", default: options.typescript || false },
+  enableDynamicImport: { env: "ENABLE_DYNAMIC_IMPORT", default: false },
+  enableFlow: { env: "ENABLE_BABEL_FLOW", default: true },
+  // require the @flow directive in source to enable FlowJS type stripping
+  flowRequireDirective: { env: "FLOW_REQUIRE_DIRECTIVE", default: false },
+  proposalDecorators: { env: "BABEL_PROPOSAL_DECORATORS", default: false },
+  legacyDecorators: { env: "BABEL_LEGACY_DECORATORS", default: true },
+  transformClassProps: { env: "BABEL_CLASS_PROPS", default: false },
+  looseClassProps: { env: "BABEL_CLASS_PROPS_LOOSE", default: true },
+  envTargets: {
+    env: "BABEL_ENV_TARGETS",
+    type: "json",
+    default: {
+      //`default` and `node` targets object is required
+      default: {
+        ie: "8"
+      },
+      node: process.versions.node.split(".")[0]
+    }
+  },
+  target: {
+    env: "ENV_TARGET",
+    type: "string",
+    default: "default"
+  },
+  // `extendLoader` is used to override `babel-loader` only when `hasMultiTargets=true`
+  extendLoader: {
+    type: "json",
+    default: {}
+  }
+};
+
+module.exports = xenvConfig(babelConfigSpec, userConfig.babel, { merge });
diff --git a/packages/xarc-app-dev/config/env-karma.js b/packages/xarc-app-dev/config/env-karma.js
new file mode 100644
index 000000000..0cf63d51c
--- /dev/null
+++ b/packages/xarc-app-dev/config/env-karma.js
@@ -0,0 +1,11 @@
+"use strict";
+
+const xenvConfig = require("xenv-config");
+const { merge } = require("lodash");
+const userConfig = require("./user-config");
+
+const karmaConfigSpec = {
+  browser: { env: "KARMA_BROWSER", default: "chrome" }
+};
+
+module.exports = xenvConfig(karmaConfigSpec, userConfig.karma, { merge });
diff --git a/packages/xarc-app-dev/config/env-proxy.js b/packages/xarc-app-dev/config/env-proxy.js
new file mode 100644
index 000000000..c6f0833fb
--- /dev/null
+++ b/packages/xarc-app-dev/config/env-proxy.js
@@ -0,0 +1,16 @@
+"use strict";
+
+const xenvConfig = require("xenv-config");
+const { merge } = require("lodash");
+
+const proxyConfigSpec = {
+  httpsPort: {
+    env: ["ELECTRODE_DEV_HTTPS", "XARC_DEV_HTTPS"],
+    default: 0,
+    envMap: { true: 443, false: 0 },
+    post: x => x || 0
+  },
+  elevated: { env: ["ELECTRODE_DEV_ELEVATED"], default: false }
+};
+
+module.exports = xenvConfig(proxyConfigSpec, {}, { merge });
diff --git a/packages/xarc-app-dev/config/env-webpack.js b/packages/xarc-app-dev/config/env-webpack.js
new file mode 100644
index 000000000..46d7f2e30
--- /dev/null
+++ b/packages/xarc-app-dev/config/env-webpack.js
@@ -0,0 +1,50 @@
+"use strict";
+
+const userConfig = require("./user-config");
+const xenvConfig = require("xenv-config");
+const { merge } = require("lodash");
+
+const webpackConfigSpec = {
+  webpackDev: { env: "WEBPACK_DEV", default: false },
+  devHostname: { env: ["WEBPACK_HOST", "WEBPACK_DEV_HOST", "HOST"], default: "localhost" },
+  devPort: { env: "WEBPACK_DEV_PORT", default: 2992 },
+  // Using a built-in reverse proxy, the webpack dev assets are served from the
+  // same host and port as the app.  In that case, the URLs to assets are relative
+  // without protocol, host, port.
+  // However, user can simulate CDN server with the proxy and have assets URLs
+  // specifying different host/port from the app.  To do that, the following
+  // should be defined.
+  cdnProtocol: { env: ["WEBPACK_DEV_CDN_PROTOCOL"], type: "string", default: null },
+  cdnHostname: { env: ["WEBPACK_DEV_CDN_HOST"], type: "string", default: null },
+  cdnPort: { env: ["WEBPACK_DEV_CDN_PORT"], default: 0 },
+  //
+  // in dev mode, all webpack output are saved to memory only, but some files like
+  // stats.json are needed by different uses and the stats partial saves a copy to
+  // disk.  It will use this as the path to save the file.
+  devArtifactsPath: { env: "WEBPACK_DEV_ARTIFACTS_PATH", default: ".etmp" },
+  cssModuleSupport: { env: "CSS_MODULE_SUPPORT", type: "boolean", default: undefined },
+  enableBabelPolyfill: { env: "ENABLE_BABEL_POLYFILL", default: false },
+  enableNodeSourcePlugin: { env: "ENABLE_NODESOURCE_PLUGIN", default: false },
+  enableHotModuleReload: { env: "WEBPACK_HOT_MODULE_RELOAD", default: true },
+  enableWarningsOverlay: { env: "WEBPACK_DEV_WARNINGS_OVERLAY", default: true },
+  woffFontInlineLimit: { env: "WOFF_FONT_INLINE_LIMIT", default: 1000 },
+  preserveSymlinks: {
+    env: ["WEBPACK_PRESERVE_SYMLINKS", "NODE_PRESERVE_SYMLINKS"],
+    default: false
+  },
+  enableShortenCSSNames: { env: "ENABLE_SHORTEN_CSS_NAMES", default: true },
+  minimizeSubappChunks: { env: "MINIMIZE_SUBAPP_CHUNKS", default: false },
+  optimizeCssOptions: {
+    env: "OPTIMIZE_CSS_OPTIONS",
+    type: "json",
+    default: {
+      cssProcessorOptions: {
+        zindex: false
+      }
+    }
+  },
+  loadDlls: { env: "ELECTRODE_LOAD_DLLS", type: "json", default: {} },
+  minify: { env: "WEBPACK_MINIFY", default: true }
+};
+
+module.exports = xenvConfig(webpackConfigSpec, userConfig.webpack, { merge });
diff --git a/packages/xarc-app-dev/config/user-config.js b/packages/xarc-app-dev/config/user-config.js
new file mode 100644
index 000000000..fbcb1cbd6
--- /dev/null
+++ b/packages/xarc-app-dev/config/user-config.js
@@ -0,0 +1,7 @@
+"use strict";
+
+const Path = require("path");
+const { merge } = require("lodash")
+
+const optionalRequire = require("optional-require")(require);
+module.exports = merge({ options: {} }, optionalRequire(Path.resolve("archetype/config")));
diff --git a/packages/xarc-app-dev/lib/app-dev-middleware.js b/packages/xarc-app-dev/lib/app-dev-middleware.js
index 5a8d66db3..a3de75c2c 100644
--- a/packages/xarc-app-dev/lib/app-dev-middleware.js
+++ b/packages/xarc-app-dev/lib/app-dev-middleware.js
@@ -59,7 +59,7 @@ class AppDevMiddleware {
       }
     });
     // notify dev-admin that app server started
-    process.nextTick(() => process.send({ name: "app-setup" }));
+    process.nextTick(() => process.send && process.send({ name: "app-setup" }));
   }
 }
 
diff --git a/packages/xarc-app-dev/lib/dev-admin/admin-server.js b/packages/xarc-app-dev/lib/dev-admin/admin-server.js
index c6889824d..7b1c38484 100644
--- a/packages/xarc-app-dev/lib/dev-admin/admin-server.js
+++ b/packages/xarc-app-dev/lib/dev-admin/admin-server.js
@@ -16,13 +16,14 @@ const { fork } = require("child_process");
 const ConsoleIO = require("./console-io");
 const logger = require("@xarc/app/lib/logger");
 const xaa = require("xaa");
+const {
+  settings: { useDevProxy: DEV_PROXY_ENABLED }
+} = require("../../config/dev-proxy");
 
 const APP_SERVER_NAME = "your app server";
 const DEV_SERVER_NAME = "Electrode webpack dev server";
 const PROXY_SERVER_NAME = "Electrode Dev Proxy";
 
-const DEV_PROXY_ENABLED = Boolean(process.env.APP_SERVER_PORT);
-
 class AdminServer {
   constructor(args, options) {
     this._opts = args.opts;
diff --git a/packages/xarc-app-dev/lib/dev-admin/log-parser.js b/packages/xarc-app-dev/lib/dev-admin/log-parser.js
index 16e9a2af8..5c9d76506 100644
--- a/packages/xarc-app-dev/lib/dev-admin/log-parser.js
+++ b/packages/xarc-app-dev/lib/dev-admin/log-parser.js
@@ -23,15 +23,15 @@ const BunyanLevelLookup = {
 };
 const parsers = [
   {
-    custom: (raw) => raw.match(UnhandledRejection) ? [raw, "error", raw] : undefined,
+    custom: raw => (raw.match(UnhandledRejection) ? [raw, "error", raw] : undefined),
     prefix: ""
   },
-  {regex: LogParse, prefix: ""},
+  { regex: LogParse, prefix: "" },
   {
-    custom: (raw) => raw.match(NodeParse) ? [raw, "warn", raw] : undefined,
+    custom: raw => (raw.match(NodeParse) ? [raw, "warn", raw] : undefined),
     prefix: NodeDebuggerTag
   },
-  {regex: FyiLogParse, prefix: FyiTag}
+  { regex: FyiLogParse, prefix: FyiTag }
 ];
 
 function parseRegex(raw, parser) {
diff --git a/packages/xarc-app-dev/lib/dev-admin/log-reader.js b/packages/xarc-app-dev/lib/dev-admin/log-reader.js
index 9a81d399f..4616b1fe8 100644
--- a/packages/xarc-app-dev/lib/dev-admin/log-reader.js
+++ b/packages/xarc-app-dev/lib/dev-admin/log-reader.js
@@ -42,12 +42,12 @@ const Levels = {
 };
 
 async function getLogsByLine(maxLevel = DefaultMaxLevel, handleLogLine) {
-  return new Promise((resolve) => {
+  return new Promise(resolve => {
     const readInterface = readline.createInterface({
       input: fs.createReadStream("archetype-debug.log")
     });
 
-    readInterface.on("line", (event) => {
+    readInterface.on("line", event => {
       event = JSON.parse(event);
       const levelInfo = Levels[event.level];
       if (levelInfo.index > maxLevel) {
@@ -61,15 +61,13 @@ async function getLogsByLine(maxLevel = DefaultMaxLevel, handleLogLine) {
 
 async function getLogs(maxLevel = DefaultMaxLevel) {
   const logs = [];
-  await getLogsByLine(maxLevel, (event) => logs.push(event));
+  await getLogsByLine(maxLevel, event => logs.push(event));
   return logs;
 }
 
 function getLogEventAsAnsi(event) {
   const levelInfo = Levels[event.level];
-  const name = levelInfo.color
-    ? ck(`<${levelInfo.color}>${levelInfo.name}</>`)
-    : levelInfo.name;
+  const name = levelInfo.color ? ck(`<${levelInfo.color}>${levelInfo.name}</>`) : levelInfo.name;
   return `${name}: ${event.message}`;
 }
 
@@ -83,7 +81,7 @@ function getLogEventAsHtml(event) {
 
 // eslint-disable-next-line no-console
 async function displayLogs(maxLevel = DefaultMaxLevel, show = console.log) {
-  await getLogsByLine(maxLevel, (event) => show(getLogEventAsAnsi(event, show)));
+  await getLogsByLine(maxLevel, event => show(getLogEventAsAnsi(event, show)));
 }
 
 module.exports = {
diff --git a/packages/xarc-app-dev/lib/dev-admin/middleware.js b/packages/xarc-app-dev/lib/dev-admin/middleware.js
index 452b459a0..1d43f6bdd 100644
--- a/packages/xarc-app-dev/lib/dev-admin/middleware.js
+++ b/packages/xarc-app-dev/lib/dev-admin/middleware.js
@@ -10,6 +10,8 @@ const hotHelpers = require("webpack-hot-middleware/helpers");
 const Url = require("url");
 const { getWebpackStartConfig } = require("@xarc/webpack/lib/util/custom-check");
 const { getLogs, getLogEventAsHtml } = require("./log-reader");
+const { fullDevServer, controlPaths } = require("../../config/dev-proxy");
+const { formUrl } = require("../utils");
 
 hotHelpers.pathMatch = (url, path) => {
   try {
@@ -23,12 +25,10 @@ const webpackDevMiddleware = require("webpack-dev-middleware");
 const webpackHotMiddleware = require("webpack-hot-middleware");
 const serveIndex = require("serve-index-fs");
 const ck = require("chalker");
-const archetype = require("@xarc/app/config/archetype");
 const _ = require("lodash");
 const statsUtils = require("../stats-utils");
 const statsMapper = require("../stats-mapper");
-const devRequire = archetype.devRequire;
-const xsh = devRequire("xsh");
+const xsh = require("xsh");
 const shell = xsh.$;
 
 function urlJoin() {
@@ -77,15 +77,12 @@ class Middleware {
 
     const config = require(getWebpackStartConfig("webpack.config.dev.js"));
 
-    const { devPort, devHostname } = archetype.webpack;
-
-    // this is webpack-hot-middleware's default
-    this._hmrPath = "/__webpack_hmr";
+    this._hmrPath = controlPaths.hmr;
 
     const webpackHotOptions = _.merge(
       {
         log: false,
-        path: `http://${devHostname}:${devPort}${this._hmrPath}`,
+        path: formUrl({ ...fullDevServer, path: this._hmrPath }),
         heartbeat: 2000
       },
       options.hot
@@ -158,7 +155,7 @@ class Middleware {
     });
 
     this.cwdIndex = serveIndex(process.cwd(), { icons: true, hidden: true });
-    this.devBaseUrl = urlJoin(options.devBaseUrl || "/__electrode_dev");
+    this.devBaseUrl = urlJoin(options.devBaseUrl || controlPaths.dev);
     this.devBaseUrlSlash = urlJoin(this.devBaseUrl, "/");
     this.cwdBaseUrl = urlJoin(this.devBaseUrl, "/cwd");
     this.cwdContextBaseUrl = urlJoin(this.devBaseUrl, "/memfs");
diff --git a/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js b/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js
index 6fad7fe6a..360295e38 100644
--- a/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js
+++ b/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js
@@ -2,229 +2,287 @@
 
 /* eslint-disable max-statements, no-process-exit, global-require, no-console */
 
-const Url = require("url");
+const assert = require("assert");
 const Path = require("path");
 const _ = require("lodash");
 const redbird = require("@jchip/redbird");
 const ck = require("chalker");
 const optionalRequire = require("optional-require")(require);
+const { settings, certs: proxyCerts, controlPaths } = require("../../config/dev-proxy");
 
-const getIntFromEnv = (name, defaultVal) => {
-  const envKey = [].concat(name).find(x => process.env[x]);
-  return parseInt(process.env[envKey] || (defaultVal && defaultVal.toString()), 10);
-};
+const { formUrl } = require("../utils");
 
-const getHost = () => {
-  return process.env.HOST || "localhost";
-};
+let APP_RULES = [];
 
 const getNotFoundPage = data => {
-  const { actualHost, expectedHost, port } = data;
+  const { protocol, actualHost, expectedHost, port } = data;
+  const adminUrl = formUrl({ protocol, host: expectedHost, port, path: controlPaths.status });
   /* eslint-disable max-len */
-  const invalidHostMessage = `
-    <p>
-      <span style="color: red;">ERROR:</span> You used a hostname that the development proxy doesn't recognize.<br />
-      The proxy is using the hostname <span style="color: green;">${expectedHost}</span>.<br />
-      The hostname in your URL is <span style="color: red;">${actualHost}</span>.<br />
-      To change the hostname that the proxy uses, set the HOST env variable.<br />
-      For example:<br /><br />
-      In unix bash: <code style="color: white;background-color: black;padding: 5px;">
-        HOST=${actualHost} clap dev
-      </code><br /><br />
-      In Windows PowerShell: <code style="color: white;background-color: #012456;padding: 5px;">
-        $Env:HOST="${actualHost}"; clap dev
-      </code>
-    </p>
-  `;
-  return `
-    <html>
-      <body>
-        <div style="text-align:center;">
-          <div>
-            <svg version="1.0" viewBox="100 0 202.3 65.1" width="250px" height="250px">
-              <g transform="scale(5,5) translate(-94, -10)">
-                <path id="Fill-1" class="st0" d="M134.6,8.9c-0.2,0-0.3-0.1-0.4-0.2l-1.1-1.9l-0.9,1.8c-0.1,0.2-0.4,0.3-0.6,0.2 c-0.1-0.1-0.2-0.2-0.2-0.3l-0.8-4.6l-0.9,2.7c-0.1,0.2-0.2,0.3-0.4,0.3h-2.2c-0.2,0-0.4-0.2-0.4-0.4c0-0.2,0.2-0.4,0.4-0.4 h1.9l1.3-4.1c0.1-0.2,0.3-0.3,0.5-0.3c0.1,0,0.3,0.2,0.3,0.3l0.9,5.1l0.7-1.3c0.1-0.2,0.4-0.3,0.6-0.2c0.1,0,0.1,0.1,0.2,0.2 l1,1.6l1.6-7c0.1-0.2,0.3-0.4,0.5-0.3c0.2,0,0.3,0.2,0.3,0.3l1,5.7l3.4,0.2c0.2,0,0.4,0.2,0.4,0.4c0,0.2-0.2,0.4-0.4,0.4l0,0 l-3.7-0.2c-0.2,0-0.4-0.2-0.4-0.3l-0.8-4L135,8.6C135,8.7,134.8,8.9,134.6,8.9L134.6,8.9"></path>
-                <path d="M134.3,31.9 c-4.9,0 -8.8,-3.9 -8.8,-8.8 s3.9,-8.8 8.8,-8.8 s8.8,3.9 8.8,8.8 S139.2,31.9 134.3,31.9 L134.3,31.9 M145.1,18.5 h-0.3 l-0.1,-0.3 c-0.6,-1.3 -1.5,-2.4 -2.5,-3.4 l-0.4,-0.3 l3.4,-5.8 h0.3 c1,-0.2 1.6,-1.1 1.5,-2 s-1.1,-1.6 -2,-1.5 s-1.6,1.1 -1.5,2 c0,0.2 0.1,0.4 0.2,0.6 l0.2,0.3 l-3.3,5.4 l-0.5,-0.3 c-1.7,-0.9 -3.6,-1.5 -5.6,-1.5 l0,0 c-1.9,0 -3.9,0.5 -5.6,1.5 l-0.5,0.3 L124.9,8 l0.1,-0.3 c0.1,-0.2 0.2,-0.5 0.2,-0.8 c0,-1 -0.8,-1.8 -1.8,-1.8 s-1.8,0.8 -1.8,1.8 c0,0.9 0.7,1.6 1.5,1.7 h0.3 l3.4,5.8 l-0.4,0.3 c-1,0.9 -1.9,2.1 -2.5,3.3 l-0.2,0.3 h-0.3 c-2.4,0.2 -4.2,2.4 -4,4.8 c0.2,2 1.7,3.7 3.8,4 h0.3 l0.1,0.3 c1.8,4.2 5.9,7 10.5,7 l0,0 c4.6,0 8.7,-2.8 10.5,-7 l0.1,-0.3 h0.3 c2.4,-0.4 4.1,-2.6 3.7,-5 C148.7,20.2 147.1,18.6 145.1,18.5 " id="Fill-4" class="st0"></path>
-                <path id="Fill-7" class="st0" d="M138.9,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3 S138.9,20.4,138.9,21.1C138.9,21.1,138.9,21.1,138.9,21.1"></path>
-                <path id="Fill-9" class="st0" d="M132.2,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3c0,0,0,0,0,0 C131.7,19.8,132.3,20.4,132.2,21.1C132.3,21.1,132.3,21.1,132.2,21.1"></path>
-              </g>
-            </svg>
-            <p>
-              Electrode development reverse proxy is unable to forward your URL.<br />
-              Check <a href="http://${expectedHost}:${port}/__proxy_admin/status">
-                http://${expectedHost}:${port}/__proxy_admin/status
-              </a> to see a list of forward rules.
-            </p>
-            ${actualHost !== expectedHost ? invalidHostMessage : ""}
-          </div>
-        </div>
-      </body>
-    </html>
-  `;
+  const invalidHostMessage = `<p>
+  <span style="color: red;">ERROR:</span> You used a hostname that the development proxy doesn't recognize.<br />
+  The proxy is using the hostname <span style="color: green;">${expectedHost}</span>.<br />
+  The hostname in your URL is <span style="color: red;">${actualHost}</span>.<br />
+  To change the hostname that the proxy uses, set the HOST env variable.<br />
+  For example:<br /><br />
+  In unix bash: <code style="color: white;background-color: black;padding: 5px;">
+    HOST=${actualHost} clap dev
+  </code><br /><br />
+  In Windows PowerShell: <code style="color: white;background-color: #012456;padding: 5px;">
+    $Env:HOST="${actualHost}"; clap dev
+  </code>
+</p>`;
+  return `<html>
+  <body>
+    <div style="text-align:center;">
+      <div>
+        <svg version="1.0" viewBox="100 0 202.3 65.1" width="250px" height="250px">
+          <g transform="scale(5,5) translate(-94, -10)">
+            <path id="Fill-1" class="st0" d="M134.6,8.9c-0.2,0-0.3-0.1-0.4-0.2l-1.1-1.9l-0.9,1.8c-0.1,0.2-0.4,0.3-0.6,0.2 c-0.1-0.1-0.2-0.2-0.2-0.3l-0.8-4.6l-0.9,2.7c-0.1,0.2-0.2,0.3-0.4,0.3h-2.2c-0.2,0-0.4-0.2-0.4-0.4c0-0.2,0.2-0.4,0.4-0.4 h1.9l1.3-4.1c0.1-0.2,0.3-0.3,0.5-0.3c0.1,0,0.3,0.2,0.3,0.3l0.9,5.1l0.7-1.3c0.1-0.2,0.4-0.3,0.6-0.2c0.1,0,0.1,0.1,0.2,0.2 l1,1.6l1.6-7c0.1-0.2,0.3-0.4,0.5-0.3c0.2,0,0.3,0.2,0.3,0.3l1,5.7l3.4,0.2c0.2,0,0.4,0.2,0.4,0.4c0,0.2-0.2,0.4-0.4,0.4l0,0 l-3.7-0.2c-0.2,0-0.4-0.2-0.4-0.3l-0.8-4L135,8.6C135,8.7,134.8,8.9,134.6,8.9L134.6,8.9"></path>
+            <path d="M134.3,31.9 c-4.9,0 -8.8,-3.9 -8.8,-8.8 s3.9,-8.8 8.8,-8.8 s8.8,3.9 8.8,8.8 S139.2,31.9 134.3,31.9 L134.3,31.9 M145.1,18.5 h-0.3 l-0.1,-0.3 c-0.6,-1.3 -1.5,-2.4 -2.5,-3.4 l-0.4,-0.3 l3.4,-5.8 h0.3 c1,-0.2 1.6,-1.1 1.5,-2 s-1.1,-1.6 -2,-1.5 s-1.6,1.1 -1.5,2 c0,0.2 0.1,0.4 0.2,0.6 l0.2,0.3 l-3.3,5.4 l-0.5,-0.3 c-1.7,-0.9 -3.6,-1.5 -5.6,-1.5 l0,0 c-1.9,0 -3.9,0.5 -5.6,1.5 l-0.5,0.3 L124.9,8 l0.1,-0.3 c0.1,-0.2 0.2,-0.5 0.2,-0.8 c0,-1 -0.8,-1.8 -1.8,-1.8 s-1.8,0.8 -1.8,1.8 c0,0.9 0.7,1.6 1.5,1.7 h0.3 l3.4,5.8 l-0.4,0.3 c-1,0.9 -1.9,2.1 -2.5,3.3 l-0.2,0.3 h-0.3 c-2.4,0.2 -4.2,2.4 -4,4.8 c0.2,2 1.7,3.7 3.8,4 h0.3 l0.1,0.3 c1.8,4.2 5.9,7 10.5,7 l0,0 c4.6,0 8.7,-2.8 10.5,-7 l0.1,-0.3 h0.3 c2.4,-0.4 4.1,-2.6 3.7,-5 C148.7,20.2 147.1,18.6 145.1,18.5 " id="Fill-4" class="st0"></path>
+            <path id="Fill-7" class="st0" d="M138.9,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3 S138.9,20.4,138.9,21.1C138.9,21.1,138.9,21.1,138.9,21.1"></path>
+            <path id="Fill-9" class="st0" d="M132.2,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3c0,0,0,0,0,0 C131.7,19.8,132.3,20.4,132.2,21.1C132.3,21.1,132.3,21.1,132.2,21.1"></path>
+          </g>
+        </svg>
+        <p>
+          Electrode development reverse proxy is unable to forward your URL.<br />
+          Check <a href="${adminUrl}">${adminUrl}</a> to see a list of forward rules.
+        </p>
+        ${actualHost !== expectedHost ? invalidHostMessage : ""}
+      </div>
+    </div>
+  </body>
+</html>`;
   /* eslint-enable max-len */
 };
 
 // eslint-disable-next-line max-params
-const makeUrl = (host, port, pathname = "", protocol = "http") => {
-  // eslint-disable-next-line
-  if (port !== 80) {
-    host = `${host}:${port}`;
-  }
-  return Url.format({
-    protocol,
+const getLineForTree = (protocol, host) => (tree, [envVarName, port], idx, arr) => {
+  const boxChar = idx === arr.length - 1 ? "└" : "├";
+  const portText = envVarName.replace(/port/gi, "");
+  return ck`${tree}  ${boxChar}─<green>${formUrl({
+    protocol: "http",
     host,
-    port: `${port}`,
-    pathname
-  });
+    port
+  })}</> (${portText})\n`;
 };
 
-// eslint-disable-next-line max-params
-const getLineForTree = (host) => (tree, [envVarName, port], idx, arr) => {
-  const boxChar = idx === arr.length - 1 ? "└" : "├";
-
-  return `${tree}  ${boxChar}─http://${host}:${port} (${envVarName.replace(/port/ig, "")})\n`;
-};
+const buildProxyTree = (options, ports) => {
+  const { host, port, protocol } = options;
 
-const buildProxyTree = (options) => {
-  const {host, port} = options;
-  const portTree = Object.entries(options)
-                         .filter(([k]) => k !== "port" && k.match(/port/ig))
-                         .reduce(getLineForTree(host), "");
+  const lineGen = getLineForTree(protocol, host);
+  const portTree = Object.entries(_.pick(options, ports)).reduce(lineGen, "");
 
-  return `http://${host}:${port} (proxy) \n${portTree}`;
+  return ck`<orange>${formUrl({ protocol, host, port })}</> (proxy) \n${portTree}`;
 };
 
-const startProxy = options => {
-  options = Object.assign(
-    {
-      host: getHost(),
-      port: getIntFromEnv("PORT", "3000"),
-      appPort: getIntFromEnv("APP_SERVER_PORT", "3001"),
-      webpackDevPort: getIntFromEnv(["WEBPACK_PORT", "WEBPACK_DEV_PORT"], "2992"),
-      xfwd: false,
-      bunyan: {
-        level: "warn",
-        name: "redbird"
-      },
-      resolvers: []
-    },
-    options
-  );
-
-  const proxyOptions = _.pick(options, ["port", "xfwd", "bunyan", "resolvers"]);
-  const { host, port } = options;
-
-  let proxy = redbird(proxyOptions);
-
-  proxy.notFound((req, res) => {
-    res.statusCode = 404;
-    res.setHeader("Content-Type", "text/html");
-    res.write(
-      getNotFoundPage({
-        actualHost: req.headers.host.split(":")[0],
-        expectedHost: getHost(),
-        port: getIntFromEnv("PORT", "3000")
-      })
-    );
-    res.end();
-  });
-
-  const { appPort, webpackDevPort } = options;
-
-  const rules = [
-    [makeUrl(host, port), makeUrl(host, appPort)],
-    [makeUrl(host, port, `/js`), makeUrl(host, webpackDevPort, `/js`)],
-    [makeUrl(host, port, `/__electrode_dev`), makeUrl(host, webpackDevPort, `/__electrode_dev`)],
-    [makeUrl("127.0.0.1", port), makeUrl(host, appPort)],
-    [makeUrl(`127.0.0.1`, port, `/js`), makeUrl(host, webpackDevPort, `/js`)],
-    [
-      makeUrl(`127.0.0.1`, port, `/__electrode_dev`),
-      makeUrl(host, webpackDevPort, `/__electrode_dev`)
-    ]
-  ];
-
-  rules.forEach(x => proxy.register(...x));
-
-  proxy.register({
-    src: `${host}/__proxy_admin/exit`,
-    target: `http://localhost:29999/skip`,
-    onRequest: (req, res) => {
-      res.statusCode = 200;
-      res.write(`<!DOCTYPE html>
+const onExitRequest = (req, res) => {
+  res.statusCode = 200;
+  res.write(`<!DOCTYPE html>
 <html><head></head><body>
 <h1>Bye</h1>
 </body>
 </html>`);
 
-      res.end();
-      process.nextTick(() => process.exit(0));
+  res.end();
+  process.nextTick(() => process.exit(0));
 
-      return false;
-    }
-  });
+  return false;
+};
 
-  proxy.register({
-    src: `${host}/__proxy_admin/status`,
-    target: `http://localhost:29999/skip`,
-    onRequest: (req, res) => {
-      res.statusCode = 200;
-      res.write(`<!DOCTYPE html>
+const onStatusRequest = (req, res) => {
+  res.statusCode = 200;
+  res.write(`<!DOCTYPE html>
 <html><head></head><body>
 <h1>Electrode Development Reverse Proxy</h1>
 <h2>Rules</h2>
 <ul>
-${rules
-  .map(
-    x => `<li><pre><a href="${x[0]}">${x[0]}</a> ===&gt; <a href="${x[1]}">${x[1]}</a></pre></li>`
-  )
-  .join("")}
+${APP_RULES.map(
+  x => `<li><pre><a href="${x[0]}">${x[0]}</a> ===&gt; <a href="${x[1]}">${x[1]}</a></pre></li>`
+).join("")}
 </ul>
 </body>
 </html>`);
-      res.end();
+  res.end();
 
-      return false;
-    }
-  });
+  return false;
+};
 
-  const restart = () => {
-    proxy.close();
-    proxy = undefined;
-    process.nextTick(startProxy);
-  };
+const registerElectrodeDevRules = ({
+  proxy,
+  ssl,
+  protocol,
+  host,
+  port,
+  appPort,
+  webpackDevPort,
+  restart
+}) => {
+  const { dev: devPath, admin: adminPath, hmr: hmrPath, appLog, reporter } = controlPaths;
+  const appForwards = [
+    [{}, { port: appPort }],
+    [{ path: `/js` }, { path: `/js`, port: webpackDevPort }],
+    [{ path: devPath }, { path: devPath, port: webpackDevPort }],
+    [{ path: hmrPath }, { path: hmrPath, port: webpackDevPort }],
+    [{ path: appLog }, { path: appLog, port: webpackDevPort }],
+    [{ path: reporter }, { path: reporter, port: webpackDevPort }],
+    [{ path: `${adminPath}/test-google` }, { protocol: "https", host: "www.google.com" }]
+  ];
+  const appRules = appForwards
+    .map(([src, target, opts]) => {
+      return [
+        formUrl({ host, protocol, port, ...src }),
+        formUrl({ host, protocol: "http", ...target }),
+        opts
+      ];
+    })
+    .concat(
+      // repeat all rules for 127.0.0.1
+      appForwards.map(([src, target, opts]) => {
+        return [
+          formUrl({ protocol, port, ...src, host: src.host || "127.0.0.1" }),
+          formUrl({
+            ...target,
+            protocol: target.protocol || "http",
+            host: target.host || "127.0.0.1"
+          }),
+          opts
+        ];
+      })
+    )
+    .filter(x => x);
 
-  const userDevProxyFile = optionalRequire.resolve(Path.resolve("archetype/config/dev-proxy"));
+  appRules.forEach(x => proxy.register(...x));
 
-  const userDevProxy = userDevProxyFile && require(userDevProxyFile);
+  APP_RULES = APP_RULES.concat(appRules);
+
+  proxy.register({
+    ssl,
+    src: formUrl({ protocol, host, path: controlPaths.exit }),
+    target: `http://localhost:29999/skip`,
+    onRequest: onExitRequest
+  });
 
   proxy.register({
-    src: `${host}/__proxy_admin/restart`,
+    ssl,
+    src: formUrl({ protocol, host, path: controlPaths.status }),
+    target: `http://localhost:29999/skip`,
+    onRequest: onStatusRequest
+  });
+
+  proxy.register({
+    ssl,
+    src: formUrl({ protocol, host, path: controlPaths.restart }),
     target: `http://localhost:29999/skip`,
     onRequest: (req, res) => {
       res.statusCode = 200;
       res.write(`restarted`);
       res.end();
 
-      // ensure user's proxy rules are refreshed
-      delete require.cache[userDevProxyFile];
-
       process.nextTick(restart);
 
       return false;
     }
   });
+};
+
+const startProxy = inOptions => {
+  APP_RULES = [];
+  const options = Object.assign(
+    {
+      xfwd: false,
+      bunyan: {
+        level: "warn",
+        name: "redbird"
+      },
+      resolvers: []
+    },
+    settings,
+    inOptions
+  );
+
+  const proxyOptions = _.pick(options, ["port", "xfwd", "bunyan", "resolvers"]);
+  const { host, port, protocol } = options;
+
+  const ssl = Boolean(options.httpsPort);
+
+  if (ssl) {
+    assert(proxyCerts.key, "Dev Proxy can't find SSL key and certs");
+    Object.assign(proxyOptions, {
+      // We still setup a regular http rules even if HTTPS is enabled
+      port: options.httpPort,
+      host,
+      secure: true,
+      ssl: {
+        port: options.httpsPort,
+        ...proxyCerts
+      }
+    });
+  }
+
+  let proxy = redbird(proxyOptions);
+
+  const userFiles = ["archetype/config", "src", "test", "config"].map(x => `${x}/dev-proxy-rules`);
+  const userDevProxyFile = userFiles.find(f => optionalRequire.resolve(Path.resolve(f)));
+
+  const userDevProxy = userDevProxyFile && require(userDevProxyFile);
+
+  const restart = async () => {
+    // ensure user's proxy rules are refreshed
+    if (userDevProxyFile) {
+      delete require.cache[userDevProxyFile];
+    }
+    if (proxy) {
+      console.log("... Closing proxy server ...");
+      const old = proxy;
+      proxy = undefined;
+      await old.close(true);
+      process.nextTick(() => startProxy(inOptions));
+    }
+  };
+
+  proxy.notFound((req, res) => {
+    res.statusCode = 404;
+    res.setHeader("Content-Type", "text/html");
+    res.write(
+      getNotFoundPage({
+        protocol,
+        actualHost: req.headers.host.split(":")[0],
+        expectedHost: host,
+        port
+      })
+    );
+    res.end();
+  });
+
+  // register with primary protocol/host/port
+  registerElectrodeDevRules({ ...options, ssl, proxy, restart });
+
+  // if primary protocol is https, then register regular http rules at httpPort
+  if (ssl) {
+    registerElectrodeDevRules({
+      proxy,
+      protocol: "http",
+      host,
+      port: options.httpPort,
+      appPort: options.appPort,
+      webpackDevPort: options.webpackDevPort,
+      restart
+    });
+  }
 
   if (userDevProxy && userDevProxy.setupRules) {
     console.log("Calling proxy setupRules from your archetype/config/dev-proxy");
     userDevProxy.setupRules(proxy, options);
   }
 
+  const proxyUrl = formUrl({ protocol, host, port: options.port });
   console.log(
     ck`Electrode dev proxy server running:
 
-${buildProxyTree(options)}
-View status at <green>http://${host}:${options.port}/__proxy_admin/status</>`);
-  console.log(ck`You can access your app at <green>http://${host}:${options.port}</>`);
+${buildProxyTree(options, ["appPort", "webpackDevPort"])}
+View status at <green>${proxyUrl}${controlPaths.status}</>`
+  );
+  console.log(ck`You can access your app at <green>${proxyUrl}</>`);
 };
 
 module.exports = startProxy;
diff --git a/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js b/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js
index a01fbaac7..cd7187a0b 100644
--- a/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js
+++ b/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js
@@ -1,29 +1,60 @@
 "use strict";
 
-/* eslint-disable no-magic-numbers, no-process-exit, global-require, no-console */
+/* eslint-disable no-magic-numbers, no-process-exit, global-require, no-console, max-statements */
 
 const sudoPrompt = require("sudo-prompt");
 const request = require("request");
+const http = require("http");
+const Util = require("util");
+const { controlPaths, settings, httpDevServer } = require("../../config/dev-proxy");
+const proxyJs = require.resolve("./redbird-proxy");
+const { formUrl } = require("../utils");
+
+const canListenPort = async (port, host) => {
+  const server = http.createServer(() => {});
 
-const getIntFromEnv = (name, defaultVal) => {
-  const envKey = [].concat(name).find(x => process.env[x]);
-  return parseInt(process.env[envKey] || (defaultVal && defaultVal.toString()), 10);
+  try {
+    await Util.promisify(server.listen.bind(server))(port, host);
+  } catch (err) {
+    if (err.code === "EACCES") {
+      return false;
+    }
+  } finally {
+    await Util.promisify(server.close.bind(server))();
+  }
+
+  return true;
 };
 
-const proxyJs = require.resolve("./redbird-proxy");
+const isProxyRunning = async () => {
+  const { host, httpPort } = settings;
 
-const port = getIntFromEnv("PORT", 3000);
+  const statusUrl = formUrl({
+    host,
+    port: httpPort,
+    path: controlPaths.status
+  });
 
-const restartUrl = `http://localhost:${port}/__proxy_admin/restart`;
+  try {
+    await Util.promisify(request)(statusUrl);
+    return true;
+  } catch {
+    return false;
+  }
+};
 
 const handleRestart = type => {
   const restart = () => {
     console.log(`${type}Electrode dev proxy restarting`);
+    const restartUrl = formUrl({
+      ...httpDevServer,
+      path: controlPaths.restart
+    });
     request(restartUrl, (err, res, body) => {
       if (!err) {
         console.log(body);
       } else {
-        console.error(body, err);
+        console.error("Restarting failed, body:", body, "Error", err, "\nrestart URL", restartUrl);
       }
     });
   };
@@ -36,39 +67,59 @@ const handleRestart = type => {
   });
 };
 
-if (port <= 1024) {
-  const exitUrl = `http://localhost:${port}/__proxy_admin/exit`;
+async function mainSpawn() {
+  if (await isProxyRunning()) {
+    console.log("Electrode dev proxy already running - exiting.");
+    return;
+  }
 
-  const restart = () => {
-    sudoPrompt.exec(
-      `node ${proxyJs}`,
-      {
-        name: "Electrode Development Reverse Proxy"
-      },
-      (error, stdout, stderr) => {
-        console.log("stdout:", stdout);
-        if (error) {
-          console.error(error);
-          console.error("stderr:", stderr);
+  const { host, port, elevated } = settings;
+
+  let needElevated;
+
+  if (settings.port < 1024) {
+    // macOS mojave no longer need privileged access to listen on port < 1024
+    // https://news.ycombinator.com/item?id=18302380
+    // so run a simple listen on the port to check if it's needed
+    needElevated = !(await canListenPort(port));
+  }
+
+  if (elevated || needElevated) {
+    const exitUrl = formUrl({ host, port, path: controlPaths.exit });
+
+    const restart = () => {
+      sudoPrompt.exec(
+        `node ${proxyJs}`,
+        {
+          name: "Electrode Development Reverse Proxy"
+        },
+        (error, stdout, stderr) => {
+          console.log("stdout:", stdout);
+          if (error) {
+            console.error(error);
+            console.error("stderr:", stderr);
+          }
         }
-      }
-    );
-  };
+      );
+    };
 
-  const handleElevatedProxy = () => {
-    process.on("SIGINT", () => {
-      request(exitUrl, () => {
-        console.log("Elevated Electrode dev proxy terminating");
-        process.nextTick(() => process.exit(0));
+    const handleElevatedProxy = () => {
+      process.on("SIGINT", () => {
+        request(exitUrl, () => {
+          console.log("Elevated Electrode dev proxy terminating");
+          process.nextTick(() => process.exit(0));
+        });
       });
-    });
-  };
+    };
 
-  handleElevatedProxy();
-  handleRestart("Elevated ");
+    handleElevatedProxy();
+    handleRestart("Elevated ");
 
-  restart();
-} else {
-  handleRestart("");
-  require("./redbird-proxy");
+    restart();
+  } else {
+    handleRestart("");
+    require("./redbird-proxy");
+  }
 }
+
+mainSpawn();
diff --git a/packages/xarc-app-dev/lib/utils.js b/packages/xarc-app-dev/lib/utils.js
index 5073ba878..f1d54151d 100644
--- a/packages/xarc-app-dev/lib/utils.js
+++ b/packages/xarc-app-dev/lib/utils.js
@@ -1,7 +1,20 @@
 "use strict";
+const Url = require("url");
 
 const getOptRequire = require("@xarc/webpack/lib/util/get-opt-require");
 
+const formUrl = ({ protocol = "http", host = "", port = "", path = "" }) => {
+  const proto = protocol.toString().toLowerCase();
+  const sp = port.toString();
+  const host2 =
+    host && port && !(sp === "80" && proto === "http") && !(sp === "443" && proto === "https")
+      ? `${host}:${port}`
+      : host;
+
+  return Url.format({ protocol: proto, host: host2, pathname: path });
+};
+
 module.exports = {
-  getOptArchetypeRequire: getOptRequire
+  getOptArchetypeRequire: getOptRequire,
+  formUrl
 };
diff --git a/packages/xarc-app-dev/package.json b/packages/xarc-app-dev/package.json
index c0fbbbd48..665574252 100644
--- a/packages/xarc-app-dev/package.json
+++ b/packages/xarc-app-dev/package.json
@@ -13,16 +13,16 @@
   },
   "license": "Apache-2.0",
   "scripts": {
-    "test": "npm run lint && clap test",
-    "coverage": "npm run lint && clap check",
-    "lint": "eslint \"**/**/*.js\"",
+    "test": "clap test",
+    "coverage": "clap check",
     "format": "prettier --write --print-width 100 *.{js,jsx} `find . -type d -d 1 -exec echo '{}/**/*.{js,jsx}' \\; | egrep -v '(/node_modules/|/dist/|/coverage/)'`"
   },
   "files": [
     "config",
+    "dist",
     "lib",
-    "scripts",
-    "require.js"
+    "require.js",
+    "scripts"
   ],
   "author": "Electrode (http://www.electrode.io/)",
   "contributors": [
@@ -44,9 +44,9 @@
     "@babel/preset-env": "^7.1.6",
     "@babel/preset-react": "^7.0.0",
     "@babel/register": "^7.0.0",
-    "@jchip/redbird": "^1.0.0",
+    "@jchip/redbird": "^1.1.0",
     "@loadable/babel-plugin": "^5.10.0",
-    "@xarc/webpack": "../xarc-webpack",
+    "@xarc/webpack": "8.0.0",
     "ansi-to-html": "^0.6.8",
     "babel-plugin-dynamic-import-node": "^2.2.0",
     "babel-plugin-lodash": "^3.3.4",
@@ -83,14 +83,15 @@
     "webpack-hot-middleware": "^2.22.2",
     "winston": "^2.3.1",
     "xaa": "^1.2.2",
-    "xclap": "^0.2.38",
-    "xenv-config": "^1.3.0",
+    "xclap": "^0.2.45",
+    "xenv-config": "^1.3.1",
     "xsh": "^0.4.4"
   },
   "devDependencies": {
     "@xarc/app": "../xarc-app",
+    "@xarc/module-dev": "^2.0.3",
+    "babel-eslint": "^10.1.0",
     "chai": "^4.0.0",
-    "electrode-archetype-njs-module-dev": "^3.0.0",
     "electrode-archetype-opt-postcss": "../electrode-archetype-opt-postcss",
     "electrode-archetype-opt-sass": "../electrode-archetype-opt-sass",
     "electrode-archetype-opt-stylus": "../electrode-archetype-opt-stylus",
@@ -105,7 +106,9 @@
   "fyn": {
     "dependencies": {
       "electrode-node-resolver": "../electrode-node-resolver",
-      "subapp-util": "../subapp-util"
+      "subapp-util": "../subapp-util",
+      "@jchip/redbird": "../../../redbird",
+      "@xarc/webpack": "../xarc-webpack"
     },
     "devDependencies": {
       "@xarc/app": "../xarc-app",
@@ -120,10 +123,12 @@
       "text-summary"
     ],
     "exclude": [
-      "coverage",
       "*clap.js",
-      "gulpfile.js",
+      "*clap.ts",
+      "coverage",
       "dist",
+      "docs",
+      "gulpfile.js",
       "test"
     ],
     "check-coverage": false,
@@ -131,10 +136,17 @@
     "branches": 0,
     "functions": 0,
     "lines": 0,
-    "cache": true
+    "cache": true,
+    "extends": []
   },
   "publishConfig": {
     "registry": "https://registry.npmjs.org/",
     "access": "public"
+  },
+  "mocha": {
+    "require": [
+      "@xarc/module-dev/config/test/setup.js"
+    ],
+    "recursive": true
   }
 }
diff --git a/packages/xarc-app-dev/test/.eslintrc b/packages/xarc-app-dev/test/.eslintrc
deleted file mode 100644
index a4edd1e02..000000000
--- a/packages/xarc-app-dev/test/.eslintrc
+++ /dev/null
@@ -1,3 +0,0 @@
----
-extends:
-  - "../node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-test"
diff --git a/packages/xarc-app-dev/test/mocha.opts b/packages/xarc-app-dev/test/mocha.opts
deleted file mode 100644
index 022f99b50..000000000
--- a/packages/xarc-app-dev/test/mocha.opts
+++ /dev/null
@@ -1,2 +0,0 @@
---require node_modules/electrode-archetype-njs-module-dev/config/test/setup.js
---recursive
diff --git a/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js b/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js
index b55d4fd72..d40982cd2 100644
--- a/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js
+++ b/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js
@@ -19,10 +19,13 @@ describe("log-parser", function() {
   });
 
   it("should return the first level another level is detected midway through the message", () => {
-    const raw = "warn: An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json";
+    const raw =
+      "warn: An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json";
     const { level, message } = parse(raw);
     expect(level).equal("warn");
-    expect(message).equal("An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json");
+    expect(message).equal(
+      "An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json"
+    );
   });
 
   it("should detect Unhandled rejection messages as an error when they are annotated 'info'", () => {
@@ -33,7 +36,8 @@ describe("log-parser", function() {
   });
 
   it("should detect Unhandled rejection messages as an error when they are annotated 'debug'", () => {
-    const raw = "debug: Unhandled rejection (rejection id: 3): TypeError: Cannot read property 'Electrode' of undefined";
+    const raw =
+      "debug: Unhandled rejection (rejection id: 3): TypeError: Cannot read property 'Electrode' of undefined";
     const { level, message } = parse(raw);
     expect(level).equal("error");
     expect(message).equal(raw);
@@ -55,13 +59,13 @@ describe("log-parser", function() {
 
   it("should return error level and msg with badge for a node-bunyan level 50", () => {
     const raw = JSON.stringify({
-      "name": "stdout",
-      "hostname": "localhost",
-      "pid": 131072,
-      "tags": ["error"],
-      "msg": "Electrode SOARI service discovery failed",
-      "level": 50,
-      "time": "2019-11-25T23:50:20.353Z"
+      name: "stdout",
+      hostname: "localhost",
+      pid: 131072,
+      tags: ["error"],
+      msg: "Electrode SOARI service discovery failed",
+      level: 50,
+      time: "2019-11-25T23:50:20.353Z"
     });
     const { level, message } = parse(raw);
     expect(level).equal("error");
@@ -70,13 +74,13 @@ describe("log-parser", function() {
 
   it("should return silly level and msg with badge for a node-bunyan level 10", () => {
     const raw = JSON.stringify({
-      "name": "stdout",
-      "hostname": "localhost",
-      "pid": 131072,
-      "tags": ["silly"],
-      "msg": "The integers have been added together",
-      "level": 10,
-      "time": "2019-11-25T23:50:20.353Z"
+      name: "stdout",
+      hostname: "localhost",
+      pid: 131072,
+      tags: ["silly"],
+      msg: "The integers have been added together",
+      level: 10,
+      time: "2019-11-25T23:50:20.353Z"
     });
     const { level, message } = parse(raw);
     expect(level).equal("silly");
@@ -84,17 +88,22 @@ describe("log-parser", function() {
   });
 
   it("should return correct level and message with badge for an FYI warning and colon wrapped in color escape code", () => {
-    const raw = "\u001b[33mFYI warn:\u001b[39m electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json";
+    const raw =
+      "\u001b[33mFYI warn:\u001b[39m electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json";
     const { level, message } = parse(raw);
     expect(level).equal("warn");
-    expect(message).equal(`${FyiTag}electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json`);
+    expect(message).equal(
+      `${FyiTag}electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json`
+    );
   });
 
   it("should preserve color escape codes in message but not in level", () => {
-    const raw = "\u001b[33msilly\u001b[39m: Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {\"environment\": \"qa\"}";
+    const raw = `\u001b[33msilly\u001b[39m: Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {"environment": "qa"}`;
     const { level, message } = parse(raw);
     expect(level).equal("silly");
-    expect(message).equal("Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {\"environment\": \"qa\"}");
+    expect(message).equal(
+      `Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {"environment": "qa"}`
+    );
   });
 
   it("should return info for level and raw message for message if the log line has an unknown level", () => {
@@ -115,7 +124,9 @@ describe("log-parser", function() {
     const raw = "Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16";
     const { level, message } = parse(raw);
     expect(level).equal("warn");
-    expect(message).equal("[nod] Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16");
+    expect(message).equal(
+      "[nod] Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16"
+    );
   });
 
   it("should mark the inspector help message as a 'warn' so it is rendered to the console", () => {
diff --git a/packages/xarc-app-dev/xclap.js b/packages/xarc-app-dev/xclap.js
index ea371779c..65b0c2da6 100644
--- a/packages/xarc-app-dev/xclap.js
+++ b/packages/xarc-app-dev/xclap.js
@@ -1 +1 @@
-require("electrode-archetype-njs-module-dev")();
+require("@xarc/module-dev")();
diff --git a/packages/xarc-app/arch-clap.js b/packages/xarc-app/arch-clap.js
index cf357beac..831487ff1 100644
--- a/packages/xarc-app/arch-clap.js
+++ b/packages/xarc-app/arch-clap.js
@@ -873,6 +873,16 @@ Individual .babelrc files were generated for you in src/client and src/server
       }
     },
 
+    "dev-proxy": {
+      desc:
+        "Start Electrode dev reverse proxy by itself - useful for running it with sudo (options: --debug)",
+      task() {
+        const debug = this.argv.includes("--debug") ? "--inspect-brk " : "";
+        const proxySpawn = require.resolve("@xarc/app-dev/lib/dev-admin/redbird-spawn");
+        return `~(tty)$node ${debug}${proxySpawn}`;
+      }
+    },
+
     "test-server": xclap.concurrent(["lint-server", "lint-server-test"], "test-server-cov"),
     "test-watch-all": xclap.concurrent("server-admin.test", "test-frontend-dev-watch"),