diff --git a/.gitignore b/.gitignore
index f13064819488f1b136bbc6ac9e0b1326ac5124e0..63f3ad56b484ab5b28efdf6446016e0128452f41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 src/liowebrtc
 src/rtcpeerconnection
 src/signalbuddy
+src/yjs
 
 ### macOS ###
 # General
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 56e8e90ebe05d31d0722e3f9158fc3926d9b9bef..c334e6cb01556c29d2e001314e3e524aeb943ef1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,6 +20,7 @@ submodule_fetch:
       - src/liowebrtc
       - src/rtcpeerconnection
       - src/signalbuddy
+      - src/yjs
 
 node_install:
   stage: deps
@@ -33,6 +34,7 @@ node_install:
       - src/liowebrtc
       - src/rtcpeerconnection
       - src/signalbuddy
+      - src/yjs
 
 dev_node_install:
   stage: deps
@@ -46,6 +48,7 @@ dev_node_install:
       - src/liowebrtc
       - src/rtcpeerconnection
       - src/signalbuddy
+      - src/yjs
 
 check_format:
   stage: check
diff --git a/.gitmodules b/.gitmodules
index 378c66afc8eff5619a5903583531a433c601e60a..ddaddd31d0dc09cb5d1af99e79a14a5669247859 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,3 +7,6 @@
 [submodule "src/liowebrtc"]
 	path = src/liowebrtc
 	url = git@gitlab.doc.ic.ac.uk:sweng-group-15/liowebrtc.git
+[submodule "src/yjs"]
+	path = src/yjs
+	url = git@gitlab.doc.ic.ac.uk:sweng-group-15/yjs.git
diff --git a/package-lock.json b/package-lock.json
index e070bb2184dc24fa20b31f9d09a19d0467fb5f7a..15cbdc85de4de88e8475ba0f7cc92af9022f7a13 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -713,9 +713,9 @@
       }
     },
     "acorn-jsx": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz",
-      "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz",
+      "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==",
       "dev": true
     },
     "acorn-walk": {
@@ -1112,9 +1112,9 @@
       "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
     },
     "bluebird": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz",
-      "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
+      "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==",
       "dev": true
     },
     "bn.js": {
@@ -4808,13 +4808,13 @@
     "liowebrtc": {
       "version": "file:src/liowebrtc",
       "requires": {
-        "attachmediastream": "^2.0.4",
+        "attachmediastream": "^2.1.0",
         "filetransfer": "^2.0.4",
         "hark": "^1.2.0",
         "mockconsole": "0.0.1",
         "rtcpeerconnection": "file:src/rtcpeerconnection",
         "socket.io-client": "^2.3.0",
-        "webrtc-adapter": "^4.0.0",
+        "webrtc-adapter": "^7.3.0",
         "wildemitter": "^1.2.0"
       }
     },
@@ -4880,11 +4880,6 @@
       "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
-    "lodash.clonedeep": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
-    },
     "lodash.sortby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -6268,6 +6263,31 @@
         "lodash.clonedeep": "^4.3.2",
         "sdp-jingle-json": "^3.0.0",
         "wildemitter": "1.x"
+      },
+      "dependencies": {
+        "lodash.clonedeep": {
+          "version": "4.5.0",
+          "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+          "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+        },
+        "sdp-jingle-json": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/sdp-jingle-json/-/sdp-jingle-json-3.1.0.tgz",
+          "integrity": "sha512-Uu+FelZD/edNoOc64NwQP8jjbBVMggAaErGU+2cSxPZgyReJTtqtp5287p2vu7bHubERxEbiW0H1pC2fnH5GEA=="
+        },
+        "wildemitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz",
+          "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw=="
+        }
+      }
+    },
+    "rtcpeerconnection-shim": {
+      "version": "1.2.15",
+      "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
+      "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
+      "requires": {
+        "sdp": "^2.6.0"
       }
     },
     "run-async": {
@@ -6363,11 +6383,6 @@
       "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.10.0.tgz",
       "integrity": "sha512-H+VjfyQpRz9GezhshJmkXTtCAT9/2g9az3GFDPYfGOz0eAOQU1fCrL3S9Dq/eUT9FtOyLi/czdR9PzK3fKUYOQ=="
     },
-    "sdp-jingle-json": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/sdp-jingle-json/-/sdp-jingle-json-3.1.0.tgz",
-      "integrity": "sha512-Uu+FelZD/edNoOc64NwQP8jjbBVMggAaErGU+2cSxPZgyReJTtqtp5287p2vu7bHubERxEbiW0H1pC2fnH5GEA=="
-    },
     "semver": {
       "version": "6.3.0",
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -7081,9 +7096,9 @@
       "dev": true
     },
     "terser": {
-      "version": "4.3.8",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.8.tgz",
-      "integrity": "sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ==",
+      "version": "4.3.9",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.9.tgz",
+      "integrity": "sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA==",
       "dev": true,
       "requires": {
         "commander": "^2.20.0",
@@ -7307,9 +7322,9 @@
       "dev": true
     },
     "uglify-js": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz",
-      "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==",
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.2.tgz",
+      "integrity": "sha512-+gh/xFte41GPrgSMJ/oJVq15zYmqr74pY9VoM69UzMzq9NFk4YDylclb1/bhEzZSaUQjbW5RvniHeq1cdtRYjw==",
       "dev": true,
       "optional": true,
       "requires": {
@@ -7552,9 +7567,9 @@
       "dev": true
     },
     "webpack": {
-      "version": "4.41.1",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.1.tgz",
-      "integrity": "sha512-ak7u4tUu/U63sCVxA571IuPZO/Q0pZ9cEXKg+R/woxkDzVovq57uB6L2Hlg/pC8LCU+TWpvtcYwsstivQwMJmw==",
+      "version": "4.41.2",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz",
+      "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==",
       "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.8.5",
@@ -7688,11 +7703,12 @@
       }
     },
     "webrtc-adapter": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-4.2.2.tgz",
-      "integrity": "sha1-F4lsBHCE/UxWeVigzUMh4X8ydzw=",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.3.0.tgz",
+      "integrity": "sha512-pKcwt6IR6RLCD6jlcdOOi88iVwdzppHlkOhtgTSuZHtYTxdD09t5fA1Di7GJU7je8oHcCBlNfb7zwBsetERnmQ==",
       "requires": {
-        "sdp": "^2.1.0"
+        "rtcpeerconnection-shim": "^1.2.15",
+        "sdp": "^2.10.0"
       }
     },
     "whatwg-encoding": {
@@ -7905,9 +7921,7 @@
       "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
     },
     "yjs": {
-      "version": "12.3.3",
-      "resolved": "https://registry.npmjs.org/yjs/-/yjs-12.3.3.tgz",
-      "integrity": "sha1-e+wU1Zr+Fm1ozCsnQTGTwOW6ckw=",
+      "version": "file:src/yjs",
       "requires": {
         "debug": "^2.6.3"
       }
diff --git a/package.json b/package.json
index f63bbd65ea08152fe06ae877e4db5722d85b0755..2d7731f14ed8d7c64462f24f3610549c3c2db076 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,9 @@
   "scripts": {
     "build": "webpack src/app.js -o public/js/app.js",
     "start": "node --experimental-modules src/server.js",
-    "test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy",
-    "test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy",
-    "test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy",
+    "test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs",
+    "test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs",
+    "test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs",
     "lint": "jshint .",
     "validate": "npm ls"
   },
@@ -22,11 +22,11 @@
     "liowebrtc": "file:src/liowebrtc",
     "signalbuddy": "file:src/signalbuddy",
     "uuid": "^3.3.3",
-    "webrtc-adapter": "^4.0.0",
+    "webrtc-adapter": "^7.3.0",
     "y-array": "^10.1.4",
     "y-map": "^10.1.3",
     "y-memory": "^8.0.9",
-    "yjs": "^12.3.3"
+    "yjs": "file:src/yjs"
   },
   "devDependencies": {
     "eslint": "^6.5.1",
diff --git a/public/index.html b/public/index.html
index 27fb261049f824a9d5b36554cad399c35c59b644..d023da56ddca6ac2ddee27dd0ae12f8299bba802 100644
--- a/public/index.html
+++ b/public/index.html
@@ -18,6 +18,11 @@
           )
       }
     </script>
+    <style>
+      button.selected {
+        background-color: lightgray;
+      }
+    </style>
   </head>
   <body>
     <div>
@@ -31,15 +36,23 @@
       Connected peers:
       <ul id="connected-peers"></ul>
     </div>
-    <button id="pen">Pen</button>
-    <button id="eraser">Eraser</button>
+    <button id="pen-tool" class="selected">Pen</button>
+    <button id="eraser-tool">Eraser</button>
 
-    <svg
-      id="whiteboard"
-      width="100%"
-      height="100%"
-      style="position: fixed"
-    ></svg>
+    <svg id="canvas" width="100%" height="100%" style="position: fixed">
+      <defs>
+        <marker
+          id="dot"
+          markerUnits="userSpaceOnUse"
+          markerWidth="4"
+          markerHeight="4"
+          refX="2"
+          refY="2"
+        >
+          <circle cx="2" cy="2" r="2" fill="blue" />
+        </marker>
+      </defs>
+    </svg>
 
     <script src="js/app.js"></script>
   </body>
diff --git a/src/app.js b/src/app.js
index e9d3e9386fa77c66106c7ef6a06477201343abee..efa9f2bc574e3144cf69ac8d4dcb4b8fb4988772 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,284 +1,202 @@
-import { line, curveBasis } from "d3-shape"
-import uuidv4 from "uuid/v4"
-import yMemory from "y-memory"
-import yMap from "y-map"
-import yArray from "y-array"
-import Y from "yjs"
-
-import yWebrtc from "./y-webrtc/index.js"
-
-yMemory(Y)
-yMap(Y)
-yArray(Y)
-yWebrtc(Y)
-
-Y({
-  db: {
-    name: "memory",
-  },
-  connector: {
-    name: "webrtc",
-    url: "/",
-    room: "imperial",
-  },
-  share: {
-    drawing: "Map",
-  },
-}).then((y) => {
-  const userIDElem = document.getElementById("user-id")
-  const peerIDElem = document.getElementById("peer-id")
-  const peerButton = document.getElementById("peer-connect")
-  const connectedP = document.getElementById("connected-peers")
-  const penButton = document.getElementById("pen")
-  const eraserButton = document.getElementById("eraser")
-
-  userIDElem.value = y.db.userId
-
-  y.connector.onUserEvent(function(event) {
-    switch (event.action) {
-      case "userID":
-        userIDElem.value = event.id
-        break
-      case "userJoined":
-        var peerElem = document.createElement("li")
-        peerElem.innerHTML = event.user
-        connectedP.appendChild(peerElem)
-        break
-      case "userLeft":
-        for (var peer of connectedP.children) {
-          if (peer.innerHTML == event.user) {
-            connectedP.removeChild(peer)
-          }
-        }
-        break
-    }
-  })
-
-  peerButton.onclick = function() {
-    const peerID = peerIDElem.value
-
-    if (peerID == "") {
-      return
-    }
-
-    y.connector.connectToPeer(peerID)
-
-    peerIDElem.value = ""
+import { line, curveLinear } from "d3-shape"
+
+import {
+  canvas,
+  connectedPeers,
+  peerButton,
+  peerIDElem,
+  userIDElem,
+  penButton,
+  eraserButton,
+} from "./elements.js"
+import { connect } from "./room.js"
+
+// TODO: switch to curve interpolation that respects mouse points based on velocity
+const lineFn = line()
+  .x((d) => d[0])
+  .y((d) => d[1])
+  .curve(curveLinear)
+
+const tools = {
+  PEN: "pen",
+  ERASER: "eraser",
+}
+
+const STROKECOLOUR = "blue"
+const STROKERADIUS = 2
+const ERASERRADIUS = STROKERADIUS * 2
+
+const pathElems = new Map()
+
+const addOrUpdatePathElem = (id, points) => {
+  let pathElem = pathElems.get(id)
+
+  if (pathElem == null) {
+    pathElem = document.createElementNS("http://www.w3.org/2000/svg", "g")
+
+    pathElem.setAttribute("stroke", STROKECOLOUR)
+    pathElem.setAttribute("stroke-width", STROKERADIUS * 2)
+    pathElem.setAttribute("fill", "none")
+    pathElem.setAttribute("pointer-events", "none")
+    pathElem.setAttribute("marker-start", "url(#dot)")
+    pathElem.setAttribute("marker-end", "url(#dot)")
+
+    canvas.appendChild(pathElem)
+    pathElems.set(id, pathElem)
   }
 
-  // Used to check what kind of tool is selected
-  var addingLine = true
-  var removingLine = false
-
-  const CHECKRADIUS = 10
+  pathElem.innerHTML = ""
 
-  penButton.onclick = function() {
-    // If pen tool selected
-    if (!addingLine) {
-      addingLine = true
-      removingLine = false
-    }
-  }
-
-  eraserButton.onclick = function() {
-    // If eraser tool selected
-    if (!removingLine) {
-      removingLine = true
-      addingLine = false
-    }
+  if (points.length == 0) {
+    return pathElem
   }
 
-  const whiteboard = document.getElementById("whiteboard")
+  // Push a fake path split to generate the last path
+  points.push([-1, -1, false])
 
-  var input = false
-  var paths = new Map()
-  var pathID
+  let subpath = []
 
-  function createOrUpdatePath(uid, points) {
-    const lineFn = line()
-      .x((d) => d[0])
-      .y((d) => d[1])
-      .curve(curveBasis)
-
-    var path = paths.get(uid)
-
-    if (path === undefined) {
-      path = document.createElementNS("http://www.w3.org/2000/svg", "path")
-
-      path.setAttribute("stroke", "blue")
-      path.setAttribute("stroke-width", 3)
-      path.setAttribute("fill", "none")
-      path.setAttribute("pointer-events", "none")
-
-      whiteboard.appendChild(path)
-
-      paths.set(uid, path)
+  for (let point of points) {
+    if (point[0] === undefined) {
+      continue
     }
 
-    points = points.toArray().filter((point) => point !== undefined)
+    if (point[2] === false) {
+      if (subpath.length === 1) {
+        let subpathElem = document.createElementNS(
+          "http://www.w3.org/2000/svg",
+          "circle",
+        )
+
+        subpathElem.setAttribute("stroke", "none")
+        subpathElem.setAttribute("fill", STROKECOLOUR)
+        subpathElem.setAttribute("cx", subpath[0][0])
+        subpathElem.setAttribute("cy", subpath[0][1])
+        subpathElem.setAttribute("r", STROKERADIUS)
+
+        pathElem.appendChild(subpathElem)
+      } else if (subpath.length > 0) {
+        let subpathElem = document.createElementNS(
+          "http://www.w3.org/2000/svg",
+          "path",
+        )
+
+        subpathElem.setAttribute("d", lineFn(subpath))
+
+        pathElem.appendChild(subpathElem)
+      }
 
-    if (points.length <= 0) {
-      path.removeAttribute("d")
+      subpath = []
 
-      return path
+      continue
     }
 
-    path.setAttribute("d", lineFn(points))
-
-    console.log(points)
-    console.log(path)
-
-    return path
+    subpath.push(point)
   }
 
-  function removeOrUpdatePath(uid) {
-    var path = paths.get(uid)
+  return pathElem
+}
 
-    if (path !== undefined) {
-      paths.delete(path)
-    }
-  }
+const getDistance = (a, b) => {
+  return Math.sqrt(
+    (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]),
+  )
+}
 
-  whiteboard.onmousedown = function(e) {
-    input = true
-
-    const mouse = {
-      x: e.offsetX,
-      y: e.offsetY,
-    }
+connect("imperial").then((room) => {
+  let userInput = false
+  let currentTool = tools.PEN
+  let currentPathID = null
 
-    if (addingLine) {
-      pathID = uuidv4()
-      const sharedPath = y.share.drawing.set(pathID, Y.Array)
-      sharedPath.push([[mouse.x, mouse.y, true]])
-    } else if (removingLine) {
-      // Iterate over all the possible paths in the Map
-      const mapPaths = y.share.drawing.keys()
-      for (var mapPath of mapPaths) {
-        var found = false
-        // Get the array of coordinates
-        var mouseYArray = y.share.drawing.get(mapPath)
-        var mouseArray = mouseYArray.toArray()
-
-        // Check the array for current position
-        for (var i = 0; i < mouseArray.length; i++) {
-          var point = mouseArray[i]
-          if (checkRadius(point, mouse)) {
-            // Delete point
-            point[2] = false
-            // Update map
-            mouseYArray.insert(i, point)
-            y.share.drawing.set(mapPath, mouseYArray)
-            found = true
-            break
-          }
-        }
-        if (found) {
-          break
-        }
-      }
-    }
-  }
+  userIDElem.value = room.ownID || ""
+  room.addEventListener("allocateOwnID", ({ detail: id }) => {
+    userIDElem.value = id
+  })
 
-  /* Helper function that checks whether a point is within the mouse radius */
-  function checkRadius(mouseMap, mouse) {
-    var mouseMapX = mouseMap[0]
-    var mouseMapY = mouseMap[1]
-    var mouseX = mouse.x
-    var mouseY = mouse.y
-
-    for (var i = 0; i < CHECKRADIUS; i++) {
-      // Chech x-axis
-      if (mouseX + i == mouseMapX) {
-        return true
-      } else if (mouseX - i == mouseMapX) {
-        return true
-      }
+  room.addEventListener("userJoin", ({ detail: id }) => {
+    const peerElem = document.createElement("li")
+    peerElem.innerHTML = id
+    connectedPeers.appendChild(peerElem)
+  })
 
-      // Check y-axis
-      if (mouseY + i == mouseMapY) {
-        return true
-      } else if (mouseY - i == mouseMapY) {
-        return true
+  room.addEventListener("userLeave", ({ detail: id }) => {
+    for (const peerElem of connectedPeers.children) {
+      if (peerElem.innerHTML == id) {
+        connectedPeers.removeChild(peerElem)
       }
     }
+  })
 
-    return false
-  }
+  room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
+    addOrUpdatePathElem(id, points)
+  })
 
-  whiteboard.onmouseup = function() {
-    input = false
-  }
+  canvas.addEventListener("mousedown", (e) => {
+    userInput = true
 
-  whiteboard.onmousemove = function(e) {
-    const mouse = {
-      x: e.offsetX,
-      y: e.offsetY,
-    }
+    let mouse = [e.offsetX, e.offsetY]
 
-    if (input) {
-      if (addingLine) {
-        const sharedPath = y.share.drawing.get(pathID)
-        sharedPath.push([[mouse.x, mouse.y, true]])
-      } else if (removingLine) {
-        // Iterate over all the possible paths in the Map
-        const mapPaths = y.share.drawing.keys()
-        for (var mapPath of mapPaths) {
-          var found = false
-          // Get the array of coordinates
-          var mouseYArray = y.share.drawing.get(mapPath)
-          var mouseArray = mouseYArray.toArray()
-
-          // Check the array for current position
-          for (var i = 0; i < mouseArray.length; i++) {
-            var point = mouseArray[i]
-            if (checkRadius(point, mouse)) {
-              // Delete point
-              point[2] = false
-              // Update map
-              mouseYArray.insert(i, point)
-              y.share.drawing.set(mapPath, mouseYArray)
-              found = true
-              break
-            }
+    if (currentTool === tools.PEN) {
+      currentPathID = room.addPath(mouse)
+    } else if (currentTool === tools.ERASER) {
+      room.getPaths().forEach((points, pathID) => {
+        points.forEach((point, i) => {
+          if (getDistance(mouse, point) <= ERASERRADIUS) {
+            room.erasePoint(pathID, i)
           }
-          if (found) {
-            break
-          }
-        }
-      }
+        })
+      })
     }
-  }
-
-  y.share.drawing.observe(function(lineEvent) {
-    const lineID = lineEvent.name
+  })
 
-    switch (lineEvent.type) {
-      case "add":
-        createOrUpdatePath(lineID, lineEvent.value)
+  canvas.addEventListener("mouseleave", () => {
+    userInput = false
+  })
 
-        lineEvent.value.observe(function(pointEvent) {
-          switch (pointEvent.type) {
-            case "insert":
-              createOrUpdatePath(lineID, pointEvent.object)
-              break
-          }
-        })
+  canvas.addEventListener("mouseup", () => {
+    userInput = false
+  })
 
-        break
+  canvas.addEventListener("mousemove", (e) => {
+    if (!userInput) {
+      return
+    }
 
-      case "update":
-        removeOrUpdatePath(lineID)
+    let mouse = [e.offsetX, e.offsetY]
 
-        lineEvent.value.observe(function(pointEvent) {
-          switch (pointEvent.type) {
-            case "insert":
-              removeOrUpdatePath(lineID)
-              break
+    if (currentTool === tools.PEN) {
+      room.extendPath(currentPathID, mouse)
+    } else if (currentTool === tools.ERASER) {
+      room.getPaths().forEach((points, pathID) => {
+        points.forEach((point, i) => {
+          if (getDistance(mouse, point) <= ERASERRADIUS) {
+            room.erasePoint(pathID, i)
           }
         })
+      })
+    }
+  })
 
-        break
+  peerButton.addEventListener("click", () => {
+    const peerID = peerIDElem.value
+    if (peerID == "") {
+      return
     }
+    room.inviteUser(peerID)
+    peerIDElem.value = ""
+  })
+
+  penButton.addEventListener("click", () => {
+    currentTool = tools.PEN
+
+    penButton.classList.add("selected")
+    eraserButton.classList.remove("selected")
+  })
+
+  eraserButton.addEventListener("click", () => {
+    currentTool = tools.ERASER
+
+    penButton.classList.remove("selected")
+    eraserButton.classList.add("selected")
   })
 })
diff --git a/src/elements.js b/src/elements.js
new file mode 100644
index 0000000000000000000000000000000000000000..71d88d82b0e4a8d90248b063f9c54a5690d56dbc
--- /dev/null
+++ b/src/elements.js
@@ -0,0 +1,7 @@
+export const userIDElem = document.getElementById("user-id")
+export const peerIDElem = document.getElementById("peer-id")
+export const peerButton = document.getElementById("peer-connect")
+export const connectedPeers = document.getElementById("connected-peers")
+export const canvas = document.getElementById("canvas")
+export const penButton = document.getElementById("pen-tool")
+export const eraserButton = document.getElementById("eraser-tool")
diff --git a/src/liowebrtc b/src/liowebrtc
index afa8666f796bdd40cc263354917632ea671dfee2..f0b57a9258b117b97e1b793de27f05b0c9d5e51f 160000
--- a/src/liowebrtc
+++ b/src/liowebrtc
@@ -1 +1 @@
-Subproject commit afa8666f796bdd40cc263354917632ea671dfee2
+Subproject commit f0b57a9258b117b97e1b793de27f05b0c9d5e51f
diff --git a/src/room.js b/src/room.js
new file mode 100644
index 0000000000000000000000000000000000000000..367e136d9520f064ea07dbf96862e0f0a689ca2a
--- /dev/null
+++ b/src/room.js
@@ -0,0 +1,134 @@
+import uuidv4 from "uuid/v4"
+import yArray from "y-array"
+import yMap from "y-map"
+import yMemory from "y-memory"
+import Y from "yjs"
+
+import yWebrtc from "./y-webrtc/index.js"
+
+yMemory(Y)
+yMap(Y)
+yArray(Y)
+yWebrtc(Y)
+
+class Room extends EventTarget {
+  constructor(name) {
+    super()
+    this.name = name
+    this._y = null
+    this.ownID = null
+  }
+
+  addPath([x, y]) {
+    const id = uuidv4()
+    this._y.share.strokeAdd.set(id, Y.Array).push([[x, y]])
+    return id
+  }
+
+  extendPath(id, [x, y]) {
+    this._y.share.strokeAdd.get(id).push([[x, y]])
+  }
+
+  getPaths() {
+    let paths = new Map()
+
+    for (let id of this._y.share.strokeAdd.keys()) {
+      paths.set(id, this._generatePath(id))
+    }
+
+    return paths
+  }
+
+  erasePoint(id, idx) {
+    let eraseSet = this._y.share.strokeErase.get(id)
+
+    if (!eraseSet) {
+      eraseSet = this._y.share.strokeErase.set(id, Y.Map)
+    }
+
+    eraseSet.set(idx.toString(), true)
+  }
+
+  // Generate an array of points [x, y, exist] by merging the path's add and erase sets
+  _generatePath(id) {
+    let addSet = this._y.share.strokeAdd.get(id)
+
+    if (addSet === undefined) {
+      return []
+    }
+
+    let eraseSet = this._y.share.strokeErase.get(id) || { get: () => false }
+
+    return addSet
+      .toArray()
+      .map((p = [], i) => [p[0], p[1], !eraseSet.get(i.toString())])
+  }
+
+  inviteUser(id) {
+    this._y.connector.connectToPeer(id)
+  }
+
+  async _initialise() {
+    this._y = await Y({
+      db: {
+        name: "memory",
+      },
+      connector: {
+        name: "webrtc",
+        url: "/",
+        room: this.name,
+        onUserEvent: (event) => {
+          if (event.action == "userID") {
+            const { id } = event
+            this.ownID = id
+            this.dispatchEvent(new CustomEvent("allocateOwnID", { detail: id }))
+          } else if (event.action == "userJoined") {
+            const { user: id } = event
+            this.dispatchEvent(new CustomEvent("userJoin", { detail: id }))
+          } else if (event.action == "userLeft") {
+            const { user: id } = event
+            this.dispatchEvent(new CustomEvent("userLeave", { detail: id }))
+          }
+        },
+      },
+      share: {
+        strokeAdd: "Map",
+        strokeErase: "Map",
+      },
+    })
+    this._y.share.strokeAdd.observe((lineEvent) => {
+      if (lineEvent.type == "add") {
+        const points = this._generatePath(lineEvent.name)
+        const detail = { id: lineEvent.name, points }
+        this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
+        lineEvent.value.observe((pointEvent) => {
+          if (pointEvent.type == "insert") {
+            const points = this._generatePath(lineEvent.name)
+            const detail = { id: lineEvent.name, points }
+            this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
+          }
+        })
+      }
+    })
+    this._y.share.strokeErase.observe((lineEvent) => {
+      if (lineEvent.type == "add") {
+        const points = this._generatePath(lineEvent.name)
+        const detail = { id: lineEvent.name, points }
+        this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
+        lineEvent.value.observe((pointEvent) => {
+          if (pointEvent.type == "add") {
+            const points = this._generatePath(lineEvent.name)
+            const detail = { id: lineEvent.name, points }
+            this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
+          }
+        })
+      }
+    })
+  }
+}
+
+export const connect = async (roomName) => {
+  const room = new Room(roomName)
+  await room._initialise()
+  return room
+}
diff --git a/src/y-webrtc/index.js b/src/y-webrtc/index.js
index dab556d3d4a1498f17c9d08ff28d21af35ea71fa..d860ec2079d63fcf2f2216b753a3715ca62db8bb 100644
--- a/src/y-webrtc/index.js
+++ b/src/y-webrtc/index.js
@@ -14,11 +14,18 @@ function extend(Y) {
       super(y, options)
       this.webrtcOptions = options
 
+      if (options.onUserEvent) {
+        this.onUserEvent(options.onUserEvent)
+      }
       this.initialiseConnection()
+
+      window.addEventListener("unload", () => {
+        this.y.destroy()
+      })
     }
 
     initialiseConnection() {
-      const webrtc = new LioWebRTC({
+      this.webrtc = new LioWebRTC({
         url: this.webrtcOptions.url,
         dataOnly: true,
         /*network: {
@@ -27,57 +34,110 @@ function extend(Y) {
         },*/
       })
 
-      this.webrtc = webrtc
+      this.peers = new Set()
 
-      webrtc.on("ready", () => {
-        webrtc.joinRoom(this.webrtcOptions.room)
+      this.webrtc.on("ready", () => {
+        this.webrtc.joinRoom(this.webrtcOptions.room)
+      })
 
-        webrtc.connection.on("message", (data) =>
-          console.log("socket.io", data),
-        )
+      this.webrtc.on("joinedRoom", () => {
+        this.checkAndEnsureUser()
       })
 
-      webrtc.on("joinedRoom", () => {
-        const id = webrtc.getMyId()
+      this.webrtc.on("channelOpen", (dataChannel, peer) => {
+        this.checkAndEnsureUser()
+
+        // Start a handshake to ensure both sides are able to use the channel
+        function handshake(peer) {
+          const _peer = this.webrtc.getPeerById(peer.id)
+
+          if (!_peer || _peer !== peer) {
+            return
+          }
+
+          if (this.peers.has(peer.id)) {
+            return
+          }
+
+          console.log("ping", peer.id)
+
+          // Initial message in the handshake
+          this.webrtc.whisper(peer, "tw-ml", "tw")
 
-        for (let f of this.userEventListeners) {
-          f({ action: "userID", id: id })
+          setTimeout(handshake.bind(this, peer), 500)
         }
 
-        this.setUserId(id)
+        setTimeout(handshake.bind(this, peer), 100)
       })
 
-      // Cannot use createdPeer here as y-js will then try to send data before the channel is open
-      webrtc.on("channelOpen", (dataChannel, peer) => {
-        console.log(
-          "createdPeer",
-          peer.id,
-          this.webrtc.getPeers().map((peer) => peer.id),
-        )
-        this.userJoined(peer.id, "master")
-      })
+      this.webrtc.on("receivedPeerData", (type, message, peer) => {
+        this.checkAndEnsureUser()
+
+        if (message.type !== "update") {
+          console.log("receivedData", peer.id, message)
+        }
 
-      webrtc.on("receivedPeerData", (type, message, peer) => {
-        if (message.type !== "update")
-          console.log(
-            "receivedData",
-            peer.id,
-            message,
-            this.webrtc.getPeers().map((peer) => peer.id),
-          )
-        this.receiveMessage(peer.id, message)
+        if (type === "y-js") {
+          this.checkAndInsertPeer(peer.id)
+          this.receiveMessage(peer.id, message)
+        } else if (type === "tw-ml") {
+          if (message === "tw") {
+            // Response message in the handshake
+            this.webrtc.whisper(peer, "tw-ml", "ml")
+          } else if (message == "ml") {
+            // Handshake completed
+            this.checkAndInsertPeer(peer.id)
+          }
+        }
       })
 
-      webrtc.on("channelClose", (dataChannel, peer) => {
-        console.log(
-          "removedPeer",
-          peer.id,
-          this.webrtc.getPeers().map((peer) => peer.id),
-        )
-        this.userLeft(peer.id)
+      this.webrtc.on("channelClose", (dataChannel, peer) => {
+        this.checkAndEnsureUser()
+        this.checkAndRemovePeer(peer.id)
       })
     }
 
+    // Ensure that y-js is up to date on the user's id
+    checkAndEnsureUser() {
+      const id = this.webrtc.getMyId()
+
+      if (this.y.db.userId === id) {
+        return
+      }
+
+      for (let f of this.userEventListeners) {
+        f({ action: "userID", id: id })
+      }
+
+      this.setUserId(id)
+    }
+
+    // Ensure that y-js knows that the peer has joined
+    checkAndInsertPeer(uid) {
+      if (this.peers.has(uid)) {
+        return
+      }
+
+      this.peers.add(uid)
+
+      console.log("createdPeer", uid)
+
+      this.userJoined(uid, "master")
+    }
+
+    // Ensure that y-js knows that the peer has left
+    checkAndRemovePeer(uid) {
+      if (!this.peers.has(uid)) {
+        return
+      }
+
+      this.peers.delete(uid)
+
+      console.log("removedPeer", uid)
+
+      this.userLeft(uid)
+    }
+
     connectToPeer(/*uid*/) {
       // currently deprecated
     }
@@ -95,22 +155,19 @@ function extend(Y) {
     }
 
     send(uid, message) {
-      console.log(
-        "send",
-        uid,
-        message,
-        this.webrtc.getPeers().map((peer) => peer.id),
-      )
+      // y-js db transactions can send messages after a peer has disconnected
+      if (!this.peers.has(uid) || !this.webrtc.getPeerById(uid)) {
+        return
+      }
+
+      console.log("send", uid, message)
+
       this.webrtc.whisper(this.webrtc.getPeerById(uid), "y-js", message)
     }
 
     broadcast(message) {
-      if (message.type !== "update")
-        console.log(
-          "broadcast",
-          message,
-          this.webrtc.getPeers().map((peer) => peer.id),
-        )
+      if (message.type !== "update") console.log("broadcast", message)
+
       this.webrtc.shout("y-js", message)
     }
 
diff --git a/src/yjs b/src/yjs
new file mode 160000
index 0000000000000000000000000000000000000000..d30d6db92c8930e7d2381846d3c161b1a787393b
--- /dev/null
+++ b/src/yjs
@@ -0,0 +1 @@
+Subproject commit d30d6db92c8930e7d2381846d3c161b1a787393b