diff --git a/.eslintignore b/.eslintignore
index e6bd6c7..b8c8eed 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,4 @@ node_modules/
 lib
 public/
 *hterm*
-web_modules
+build
diff --git a/.eslintrc.json b/.eslintrc.json
index b237129..3799e81 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,9 @@
 {
   "parser": "@typescript-eslint/parser",
-  "plugins": ["@typescript-eslint", "prettier"],
+  "plugins": [
+    "@typescript-eslint",
+    "prettier"
+  ],
   "env": {
     "es6": true,
     "node": true,
@@ -14,8 +17,14 @@
     "prettier/@typescript-eslint"
   ],
   "rules": {
-    "linebreak-style": ["error", "unix"],
-    "arrow-parens": ["error", "as-needed"],
+    "linebreak-style": [
+      "error",
+      "unix"
+    ],
+    "arrow-parens": [
+      "error",
+      "as-needed"
+    ],
     "no-param-reassign": [
       "error",
       {
@@ -61,7 +70,10 @@
   "settings": {
     "import/resolver": {
       "node": {
-        "extensions": [".ts", ".js"]
+        "extensions": [
+          ".ts",
+          ".js"
+        ]
       }
     }
   }
diff --git a/containers/wetty/Dockerfile b/containers/wetty/Dockerfile
index 12db737..22d31dc 100644
--- a/containers/wetty/Dockerfile
+++ b/containers/wetty/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:dubnium-alpine as builder
+FROM node:current-alpine as builder
 RUN apk add -U build-base python
 WORKDIR /usr/src/app
 COPY . /usr/src/app
@@ -6,7 +6,7 @@ RUN yarn && \
     yarn build && \
     yarn install --production --ignore-scripts --prefer-offline
 
-FROM node:dubnium-alpine
+FROM node:current-alpine
 LABEL maintainer="butlerx@notthe.cloud"
 WORKDIR /usr/src/app
 ENV NODE_ENV=production
diff --git a/package.json b/package.json
index bad9809..24520d0 100644
--- a/package.json
+++ b/package.json
@@ -5,22 +5,20 @@
   "homepage": "https://github.com/butlerx/wetty",
   "license": "MIT",
   "type": "module",
-  "main": "./lib/main.js",
-  "module": "./lib/server.js",
+  "main": "./build/main.js",
+  "module": "./build/server.js",
   "files": [
-    "lib/",
-    "assets/css"
+    "build/"
   ],
   "scripts": {
-    "build": "snowpack build && tsc -p tsconfig.json",
-    "contributor": "all-contributors",
-    "dev": "NODE_ENV=development yarn build && concurrently --kill-others --success first \"tsc -p tsconfig.json \" \"snowpack dev\" \"nodemon .\"",
-    "lint": "eslint --ext .ts,.js .",
-    "prepublishOnly": "NODE_ENV=production yarn build",
+    "build": "snowpack build",
+    "dev": "snowpack dev",
+    "prepublishOnly": "snowpack  build",
     "start": "NODE_ENV=production node .",
+    "lint": "eslint --ext .ts,.js .",
+    "contributor": "all-contributors",
     "test": "mocha -r babel-register-ts src/**/*.spec.ts",
-    "postinstall": "snowpack install",
-    "clean": "rm -rf assets/css build lib web_modules node_modules"
+    "clean": "bash build.sh --clean"
   },
   "repository": {
     "type": "git",
@@ -75,7 +73,7 @@
       [
         "@snowpack/plugin-run-script",
         {
-          "cmd": "sass assets/scss:assets/css --load-path=node_modules -s compressed --no-source-map",
+          "cmd": "bash build.sh",
           "watch": "$1 --watch"
         }
       ]
diff --git a/assets/favicon.ico b/src/assets/favicon.ico
similarity index 100%
rename from assets/favicon.ico
rename to src/assets/favicon.ico
diff --git a/assets/scss/options.scss b/src/assets/scss/options.scss
similarity index 100%
rename from assets/scss/options.scss
rename to src/assets/scss/options.scss
diff --git a/assets/scss/overlay.scss b/src/assets/scss/overlay.scss
similarity index 100%
rename from assets/scss/overlay.scss
rename to src/assets/scss/overlay.scss
diff --git a/assets/scss/styles.scss b/src/assets/scss/styles.scss
similarity index 100%
rename from assets/scss/styles.scss
rename to src/assets/scss/styles.scss
diff --git a/assets/scss/terminal.scss b/src/assets/scss/terminal.scss
similarity index 100%
rename from assets/scss/terminal.scss
rename to src/assets/scss/terminal.scss
diff --git a/assets/scss/variables.scss b/src/assets/scss/variables.scss
similarity index 100%
rename from assets/scss/variables.scss
rename to src/assets/scss/variables.scss
diff --git a/src/buffer.ts b/src/buffer.ts
index 5818d51..bf65832 100644
--- a/src/buffer.ts
+++ b/src/buffer.ts
@@ -3,13 +3,13 @@ import { createInterface } from 'readline';
 ask('Enter your username');
 
 function ask(question: string): Promise<string> {
-  const r = createInterface({
+  const rlp = createInterface({
     input: process.stdin,
     output: process.stdout,
   });
   return new Promise(resolve => {
-    r.question(`${question}: `, answer => {
-      r.close();
+    rlp.question(`${question}: `, answer => {
+      rlp.close();
       resolve(answer);
     });
   });
diff --git a/src/client/client/options.ts b/src/client/client/options.ts
deleted file mode 100644
index 8a6d46f..0000000
--- a/src/client/client/options.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { isUndefined } from '../../web_modules/lodash.js';
-
-export function loadOptions(): object {
-  const defaultOptions = { fontSize: 14 };
-  try {
-    return isUndefined(localStorage.options)
-      ? defaultOptions
-      : JSON.parse(localStorage.options);
-  } catch {
-    return defaultOptions;
-  }
-}
diff --git a/src/client/dev.ts b/src/client/dev.ts
new file mode 100644
index 0000000..831a33c
--- /dev/null
+++ b/src/client/dev.ts
@@ -0,0 +1,5 @@
+caches.keys().then(cacheNames => {
+  cacheNames.forEach(cacheName => {
+    caches.delete(cacheName);
+  });
+});
diff --git a/src/client/index.ts b/src/client/index.ts
deleted file mode 100644
index a1e7b41..0000000
--- a/src/client/index.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { Terminal } from '../web_modules/xterm.js';
-import { isNull } from '../web_modules/lodash.js';
-import { FitAddon } from '../web_modules/xterm-addon-fit.js';
-import {
-  dom,
-  library,
-} from '../web_modules/@fortawesome/fontawesome-svg-core.js';
-import { faCogs } from '../web_modules/@fortawesome/free-solid-svg-icons.js';
-import Toastify from '../web_modules/toastify-js.js';
-import fileType from '../web_modules/file-type.js';
-
-import { FileDownloader } from './client/download.js';
-import { copySelected, copyShortcut } from './client/copyToClipboard.js';
-import { disconnect } from './client/disconnect.js';
-import { loadOptions } from './client/options.js';
-import { mobileKeyboard } from './client/mobile.js';
-import { overlay, terminal } from './shared/elements.js';
-import { socket } from './client/socket.js';
-import { verifyPrompt } from './shared/verify.js';
-
-// Setup for fontawesome
-library.add(faCogs);
-dom.watch();
-
-socket.on('connect', () => {
-  const term = new Terminal();
-  if (isNull(terminal)) return;
-  const fitAddon = new FitAddon();
-  term.loadAddon(fitAddon);
-  term.open(terminal);
-  const resize = (): void => {
-    fitAddon.fit();
-    socket.emit('resize', { cols: term.cols, rows: term.rows });
-  };
-
-  const options = loadOptions();
-  Object.entries(options).forEach(([key, value]) => {
-    term.setOption(key, value);
-  });
-  const code = JSON.stringify(options, null, 2);
-  const editor = document.querySelector('#options .editor');
-  if (!isNull(editor)) {
-    editor.value = code;
-    editor.addEventListener('keyup', () => {
-      try {
-        const updated = JSON.parse(editor.value);
-        const updatedCode = JSON.stringify(updated, null, 2);
-        editor.value = updatedCode;
-        editor.classList.remove('error');
-        localStorage.options = updatedCode;
-        Object.keys(updated).forEach(key => {
-          const value = updated[key];
-          term.setOption(key, value);
-        });
-        resize();
-      } catch {
-        // skip
-        editor.classList.add('error');
-      }
-    });
-    const toggle = document.querySelector('#options .toggler');
-    const optionsElem = document.getElementById('options');
-    if (!isNull(toggle) && !isNull(optionsElem)) {
-      toggle.addEventListener('click', e => {
-        optionsElem.classList.toggle('opened');
-        e.preventDefault();
-      });
-    }
-  }
-  if (!isNull(overlay)) overlay.style.display = 'none';
-  window.addEventListener('beforeunload', verifyPrompt, false);
-
-  term.attachCustomKeyEventHandler(copyShortcut);
-
-  document.addEventListener(
-    'mouseup',
-    () => {
-      if (term.hasSelection()) copySelected(term.getSelection());
-    },
-    false,
-  );
-
-  window.onresize = resize;
-  resize();
-  term.focus();
-  mobileKeyboard();
-
-  const fileDownloader = new FileDownloader((bufferCharacters: string) => {
-    let fileCharacters = bufferCharacters;
-    // Try to decode it as base64, if it fails we assume it's not base64
-    try {
-      fileCharacters = window.atob(fileCharacters);
-    } catch (err) {
-      // Assuming it's not base64...
-    }
-
-    const bytes = new Uint8Array(fileCharacters.length);
-    for (let i = 0; i < fileCharacters.length; i += 1) {
-      bytes[i] = fileCharacters.charCodeAt(i);
-    }
-
-    let mimeType = 'application/octet-stream';
-    let fileExt = '';
-    const typeData = fileType(bytes);
-    if (typeData) {
-      mimeType = typeData.mime;
-      fileExt = typeData.ext;
-    }
-    // Check if the buffer is ASCII
-    // Ref: https://stackoverflow.com/a/14313213
-    // eslint-disable-next-line no-control-regex
-    else if (/^[\x00-\xFF]*$/.test(fileCharacters)) {
-      mimeType = 'text/plain';
-      fileExt = 'txt';
-    }
-    const fileName = `file-${new Date()
-      .toISOString()
-      .split('.')[0]
-      .replace(/-/g, '')
-      .replace('T', '')
-      .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
-
-    const blob = new Blob([new Uint8Array(bytes.buffer)], {
-      type: mimeType,
-    });
-    const blobUrl = URL.createObjectURL(blob);
-
-    Toastify({
-      text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
-      duration: 10000,
-      newWindow: true,
-      gravity: 'bottom',
-      position: 'right',
-      backgroundColor: '#fff',
-      stopOnFocus: true,
-    }).showToast();
-  });
-
-  term.onData((data: string) => {
-    socket.emit('input', data);
-  });
-  term.onResize((size: string) => {
-    socket.emit('resize', size);
-  });
-  socket
-    .on('data', (data: string) => {
-      const remainingData = fileDownloader.buffer(data);
-      if (remainingData) {
-        term.write(remainingData);
-      }
-    })
-    .on('login', () => {
-      term.writeln('');
-      resize();
-    })
-    .on('logout', disconnect)
-    .on('disconnect', disconnect)
-    .on('error', (err: string | null) => {
-      if (err) disconnect(err);
-    });
-});
diff --git a/src/client/shared/elements.ts b/src/client/shared/elements.ts
index 13f476a..e54a1cf 100644
--- a/src/client/shared/elements.ts
+++ b/src/client/shared/elements.ts
@@ -1,2 +1,5 @@
 export const overlay = document.getElementById('overlay');
 export const terminal = document.getElementById('terminal');
+export const editor = document.querySelector(
+  '#options .editor',
+) as HTMLInputElement;
diff --git a/src/client/wetty.ts b/src/client/wetty.ts
new file mode 100644
index 0000000..0ed9b6b
--- /dev/null
+++ b/src/client/wetty.ts
@@ -0,0 +1,74 @@
+import _ from 'lodash';
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+import { dom, library } from '@fortawesome/fontawesome-svg-core';
+import { faCogs } from '@fortawesome/free-solid-svg-icons';
+
+import { FileDownloader } from './wetty/download.js';
+import { copySelected, copyShortcut } from './wetty/clipboard.js';
+import { disconnect } from './wetty/disconnect.js';
+import { configureTerm } from './wetty/options.js';
+import { mobileKeyboard } from './wetty/mobile.js';
+import { overlay, terminal } from './shared/elements.js';
+import { socket } from './wetty/socket.js';
+import { verifyPrompt } from './shared/verify.js';
+
+// Setup for fontawesome
+library.add(faCogs);
+dom.watch();
+
+socket.on('connect', () => {
+  const term = new Terminal();
+  if (_.isNull(terminal)) return;
+  const fitAddon = new FitAddon();
+  term.loadAddon(fitAddon);
+  term.open(terminal);
+  const resize = (): void => {
+    fitAddon.fit();
+    socket.emit('resize', { cols: term.cols, rows: term.rows });
+  };
+
+  configureTerm(term, resize);
+
+  if (!_.isNull(overlay)) overlay.style.display = 'none';
+  window.addEventListener('beforeunload', verifyPrompt, false);
+
+  term.attachCustomKeyEventHandler(copyShortcut);
+
+  document.addEventListener(
+    'mouseup',
+    event => {
+      if (term.hasSelection()) copySelected(event, term.getSelection());
+    },
+    false,
+  );
+
+  window.onresize = resize;
+  resize();
+  term.focus();
+  mobileKeyboard();
+  const fileDownloader = new FileDownloader();
+
+  term.onData((data: string) => {
+    socket.emit('input', data);
+  });
+  term.onResize((size: { cols: number; rows: number }) => {
+    socket.emit('resize', size);
+  });
+  socket
+    .on('data', (data: string) => {
+      const remainingData = fileDownloader.buffer(data);
+      if (remainingData) {
+        term.write(remainingData);
+      }
+    })
+    .on('login', () => {
+      term.writeln('');
+      resize();
+    })
+    .on('logout', disconnect)
+    .on('disconnect', disconnect)
+    .on('error', (err: string | null) => {
+      if (err) disconnect(err);
+    });
+});
diff --git a/src/client/client/copyToClipboard.ts b/src/client/wetty/clipboard.ts
similarity index 66%
rename from src/client/client/copyToClipboard.ts
rename to src/client/wetty/clipboard.ts
index efad698..4849efd 100644
--- a/src/client/client/copyToClipboard.ts
+++ b/src/client/wetty/clipboard.ts
@@ -1,7 +1,12 @@
-// NOTE text selection on double click or select
-export function copySelected(text: string): boolean {
-  if (window.clipboardData?.setData) {
-    window.clipboardData.setData('Text', text);
+/**
+ Copy text selection to clipboard on double click or select
+ @param event - the event this function is bound to eg mouseup
+ @param text - the selected text to copy
+ @returns boolean to indicate success or failure
+ */
+export function copySelected(event: Event, text: string): boolean {
+  if (event.clipboardData?.setData) {
+    event.clipboardData.setData('Text', text);
     return true;
   }
   if (
diff --git a/src/client/client/disconnect.ts b/src/client/wetty/disconnect.ts
similarity index 64%
rename from src/client/client/disconnect.ts
rename to src/client/wetty/disconnect.ts
index 8ce3113..72a6cef 100644
--- a/src/client/client/disconnect.ts
+++ b/src/client/wetty/disconnect.ts
@@ -1,11 +1,11 @@
-import { isNull, isUndefined } from '../../web_modules/lodash.js';
+import _ from 'lodash';
 import { verifyPrompt } from '../shared/verify.js';
 import { overlay } from '../shared/elements.js';
 
 export function disconnect(reason: string): void {
-  if (isNull(overlay)) return;
+  if (_.isNull(overlay)) return;
   overlay.style.display = 'block';
   const msg = document.getElementById('msg');
-  if (!isUndefined(reason) && !isNull(msg)) msg.innerHTML = reason;
+  if (!_.isUndefined(reason) && !_.isNull(msg)) msg.innerHTML = reason;
   window.removeEventListener('beforeunload', verifyPrompt, false);
 }
diff --git a/src/client/client/download.spec.ts b/src/client/wetty/download.spec.ts
similarity index 100%
rename from src/client/client/download.spec.ts
rename to src/client/wetty/download.spec.ts
diff --git a/src/client/client/download.ts b/src/client/wetty/download.ts
similarity index 62%
rename from src/client/client/download.ts
rename to src/client/wetty/download.ts
index 7db2064..0250486 100644
--- a/src/client/client/download.ts
+++ b/src/client/wetty/download.ts
@@ -1,6 +1,60 @@
+import Toastify from 'toastify-js';
+import fileType from 'file-type';
+
 const DEFAULT_FILE_BEGIN = '\u001b[5i';
 const DEFAULT_FILE_END = '\u001b[4i';
 
+function onCompleteFile(bufferCharacters: string): void {
+  let fileCharacters = bufferCharacters;
+  // Try to decode it as base64, if it fails we assume it's not base64
+  try {
+    fileCharacters = window.atob(fileCharacters);
+  } catch (err) {
+    // Assuming it's not base64...
+  }
+
+  const bytes = new Uint8Array(fileCharacters.length);
+  for (let i = 0; i < fileCharacters.length; i += 1) {
+    bytes[i] = fileCharacters.charCodeAt(i);
+  }
+
+  let mimeType = 'application/octet-stream';
+  let fileExt = '';
+  const typeData = fileType(bytes);
+  if (typeData) {
+    mimeType = typeData.mime;
+    fileExt = typeData.ext;
+  }
+  // Check if the buffer is ASCII
+  // Ref: https://stackoverflow.com/a/14313213
+  // eslint-disable-next-line no-control-regex
+  else if (/^[\x00-\xFF]*$/.test(fileCharacters)) {
+    mimeType = 'text/plain';
+    fileExt = 'txt';
+  }
+  const fileName = `file-${new Date()
+    .toISOString()
+    .split('.')[0]
+    .replace(/-/g, '')
+    .replace('T', '')
+    .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
+
+  const blob = new Blob([new Uint8Array(bytes.buffer)], {
+    type: mimeType,
+  });
+  const blobUrl = URL.createObjectURL(blob);
+
+  Toastify({
+    text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
+    duration: 10000,
+    newWindow: true,
+    gravity: 'bottom',
+    position: 'right',
+    backgroundColor: '#fff',
+    stopOnFocus: true,
+  }).showToast();
+}
+
 export class FileDownloader {
   fileBuffer: string[];
   fileBegin: string;
@@ -9,15 +63,15 @@ export class FileDownloader {
   onCompleteFileCallback: Function;
 
   constructor(
-    onCompleteFileCallback: (file: string) => void,
+    onCompleteFileCallback: Function = onCompleteFile,
     fileBegin: string = DEFAULT_FILE_BEGIN,
-    fileEnd: string = DEFAULT_FILE_END
+    fileEnd: string = DEFAULT_FILE_END,
   ) {
     this.fileBuffer = [];
-    this.onCompleteFileCallback = onCompleteFileCallback;
     this.fileBegin = fileBegin;
     this.fileEnd = fileEnd;
     this.partialFileBegin = '';
+    this.onCompleteFileCallback = onCompleteFileCallback;
   }
 
   bufferCharacter(character: string): string {
@@ -69,13 +123,13 @@ export class FileDownloader {
       this.fileBuffer.length >= this.fileBegin.length + this.fileEnd.length &&
       this.fileBuffer.slice(-this.fileEnd.length).join('') === this.fileEnd
     ) {
-      this.onCompleteFile(
+      this.onCompleteFileCallback(
         this.fileBuffer
           .slice(
             this.fileBegin.length,
-            this.fileBuffer.length - this.fileEnd.length
+            this.fileBuffer.length - this.fileEnd.length,
           )
-          .join('')
+          .join(''),
       );
       this.fileBuffer = [];
     }
@@ -93,13 +147,6 @@ export class FileDownloader {
     ) {
       return data;
     }
-    return data
-      .split('')
-      .map(this.bufferCharacter.bind(this))
-      .join('');
-  }
-
-  onCompleteFile(bufferCharacters: string): void {
-    this.onCompleteFileCallback(bufferCharacters);
+    return data.split('').map(this.bufferCharacter.bind(this)).join('');
   }
 }
diff --git a/src/client/client/mobile.ts b/src/client/wetty/mobile.ts
similarity index 83%
rename from src/client/client/mobile.ts
rename to src/client/wetty/mobile.ts
index 671092c..014c791 100644
--- a/src/client/client/mobile.ts
+++ b/src/client/wetty/mobile.ts
@@ -1,8 +1,8 @@
-import { isNull } from '../../web_modules/lodash.js';
+import _ from 'lodash';
 
 export function mobileKeyboard(): void {
   const [screen] = document.getElementsByClassName('xterm-screen');
-  if (isNull(screen)) return;
+  if (_.isNull(screen)) return;
   screen.setAttribute('contenteditable', 'true');
   screen.setAttribute('spellcheck', 'false');
   screen.setAttribute('autocorrect', 'false');
diff --git a/src/client/wetty/options.ts b/src/client/wetty/options.ts
new file mode 100644
index 0000000..2a0c8e7
--- /dev/null
+++ b/src/client/wetty/options.ts
@@ -0,0 +1,51 @@
+import _ from 'lodash';
+import type { Terminal } from 'xterm';
+
+import { editor } from '../shared/elements.js';
+
+function loadOptions(): object {
+  const defaultOptions = { fontSize: 14 };
+  try {
+    return _.isUndefined(localStorage.options)
+      ? defaultOptions
+      : JSON.parse(localStorage.options);
+  } catch {
+    return defaultOptions;
+  }
+}
+
+export function configureTerm(term: Terminal, resize: Function): void {
+  const options = loadOptions();
+  Object.entries(options).forEach(([key, value]) => {
+    term.setOption(key, value);
+  });
+  const config = JSON.stringify(options, null, 2);
+  if (!_.isNull(editor)) {
+    editor.value = config;
+    editor.addEventListener('keyup', () => {
+      try {
+        const updated = JSON.parse(editor.value);
+        const updatedConf = JSON.stringify(updated, null, 2);
+        editor.value = updatedConf;
+        editor.classList.remove('error');
+        localStorage.options = updatedConf;
+        Object.keys(updated).forEach(key => {
+          const value = updated[key];
+          term.setOption(key, value);
+        });
+        resize();
+      } catch {
+        // skip
+        editor.classList.add('error');
+      }
+    });
+    const toggle = document.querySelector('#options .toggler');
+    const optionsElem = document.getElementById('options');
+    if (!_.isNull(toggle) && !_.isNull(optionsElem)) {
+      toggle.addEventListener('click', e => {
+        optionsElem.classList.toggle('opened');
+        e.preventDefault();
+      });
+    }
+  }
+}
diff --git a/src/client/client/socket.ts b/src/client/wetty/socket.ts
similarity index 83%
rename from src/client/client/socket.ts
rename to src/client/wetty/socket.ts
index 5341c35..4195c8d 100644
--- a/src/client/client/socket.ts
+++ b/src/client/wetty/socket.ts
@@ -1,4 +1,4 @@
-import io from '../../web_modules/socket.io-client.js';
+import io from 'socket.io-client';
 
 const userRegex = new RegExp('ssh/[^/]+$');
 export const trim = (str: string): string => str.replace(/\/*$/, '');
diff --git a/src/server.ts b/src/server.ts
index 2f11e6a..4a32ff1 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -19,6 +19,7 @@ import {
 /**
  * Starts WeTTy Server
  * @name startServer
+ * @returns Promise that resolves SocketIO server
  */
 export async function startServer(
   ssh: SSH = sshDefault,
diff --git a/src/server/socketServer.ts b/src/server/socketServer.ts
index c232dca..a58a58e 100644
--- a/src/server/socketServer.ts
+++ b/src/server/socketServer.ts
@@ -5,7 +5,6 @@ import helmet from 'helmet';
 import http from 'http';
 import https from 'https';
 import isUndefined from 'lodash/isUndefined.js';
-import sassMiddleware from 'node-sass-middleware';
 import socket from 'socket.io';
 import winston from 'express-winston';
 import { join, resolve } from 'path';
@@ -15,6 +14,8 @@ import { html } from './socketServer/html.js';
 import { logger } from '../shared/logger.js';
 
 const trim = (str: string): string => str.replace(/\/*$/, '');
+const serveStatic = (path: string) =>
+  express.static(resolve(process.cwd(), 'build', path));
 
 export function server(
   { base, port, host, title, bypassHelmet }: Server,
@@ -32,28 +33,12 @@ export function server(
 
   const app = express();
   app
-    .use(
-      `${basePath}/web_modules`,
-      express.static(resolve(process.cwd(), 'web_modules')),
-    )
-    .use(
-      sassMiddleware({
-        src: resolve(process.cwd(), 'lib', 'client'),
-        dest: resolve(process.cwd(), 'assets'),
-        outputStyle: 'compressed',
-        log(severity: string, key: string, value: string) {
-          logger.log(severity, 'node-sass-middleware   %s : %s', key, value);
-        },
-      }),
-    )
-    .use(`${basePath}/assets`, express.static(resolve(process.cwd(), 'assets')))
-    .use(
-      `${basePath}/client`,
-      express.static(resolve(process.cwd(), 'lib', 'client')),
-    )
+    .use(`${basePath}/web_modules`, serveStatic('web_modules'))
+    .use(`${basePath}/assets`, serveStatic('assets'))
+    .use(`${basePath}/client`, serveStatic('client'))
     .use(winston.logger(logger))
     .use(compression())
-    .use(favicon(join('assets', 'favicon.ico')));
+    .use(favicon(join('build', 'assets', 'favicon.ico')));
   /* .use((req, res, next) => {
       if (req.path.substr(-1) === '/' && req.path.length > 1)
         res.redirect(
diff --git a/src/server/socketServer/html.ts b/src/server/socketServer/html.ts
index 219f677..47785ed 100644
--- a/src/server/socketServer/html.ts
+++ b/src/server/socketServer/html.ts
@@ -1,9 +1,13 @@
-import express from 'express';
+import type express from 'express';
+import { isDev } from '../../shared/env.js';
+
+const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty'];
+const cssFiles = ['styles', 'options', 'overlay', 'terminal'];
 
 const render = (
   title: string,
-  css: string,
-  js: string,
+  css: string[],
+  js: string[],
 ): string => `<!doctype html>
 <html lang="en">
   <head>
@@ -11,7 +15,7 @@ const render = (
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
     <title>${title}</title>
-    <link rel="stylesheet" href="${css}" />
+    ${css.map(file => `<link rel="stylesheet" href="${file}" />`).join('\n')}
   </head>
   <body>
     <div id="overlay">
@@ -24,11 +28,13 @@ const render = (
       <a class="toggler"
          href="#"
          alt="Toggle options"
-       ><i class="fas fa-cogs" /></a>
+       ><i class="fas fa-cogs"></i></a>
       <textarea class="editor"></textarea>
     </div>
     <div id="terminal"></div>
-    <script type="module" src="${js}" />
+    ${js
+      .map(file => `<script type="module" src="${file}"></script>`)
+      .join('\n')}
   </body>
 </html>`;
 
@@ -37,5 +43,9 @@ export const html = (base: string, title: string) => (
   res: express.Response,
 ) =>
   res.send(
-    render(title, `${base}/assets/styles.css`, `${base}/client/index.js`),
+    render(
+      title,
+      cssFiles.map(css => `${base}/assets/css/${css}.css`),
+      jsFiles.map(js => `${base}/client/${js}.js`),
+    ),
   );
diff --git a/src/shared/env.ts b/src/shared/env.ts
new file mode 100644
index 0000000..0bd4ecf
--- /dev/null
+++ b/src/shared/env.ts
@@ -0,0 +1 @@
+export const isDev = process.env.NODE_ENV === 'development';
diff --git a/src/shared/logger.ts b/src/shared/logger.ts
index 6419b18..6cf2c62 100644
--- a/src/shared/logger.ts
+++ b/src/shared/logger.ts
@@ -1,5 +1,7 @@
 import winston from 'winston';
 
+import { isDev } from './env.js';
+
 const { combine, timestamp, label, simple, json, colorize } = winston.format;
 
 const dev = combine(
@@ -12,10 +14,10 @@ const dev = combine(
 const prod = combine(label({ label: 'Wetty' }), timestamp(), json());
 
 export const logger = winston.createLogger({
-  format: process.env.NODE_ENV === 'development' ? dev : prod,
+  format: isDev ? dev : prod,
   transports: [
     new winston.transports.Console({
-      level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
+      level: isDev ? 'debug' : 'info',
       handleExceptions: true,
     }),
   ],
diff --git a/tsconfig.json b/tsconfig.json
index 32f5286..fb9a761 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,7 +15,7 @@
     "noImplicitThis": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,
-    "outDir": "./lib",
+    "outDir": "./build",
     "removeComments": true,
     "skipLibCheck": true,
     "sourceMap": true,