diff --git a/README.md b/README.md
index 10e06d6fa6ff8234fab9a0dd532e83a1ea492994..2866a6a1c305647c126aab4b1df48a3e51154b68 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,11 @@
-# A-Frame Social VR Demo
+# Mozilla Social Mixed Reality Client
 
-¯\\_(ツ)_/¯
\ No newline at end of file
+A prototype client demonstrating a multi-user experience in WebVR. Built with [A-Frame](https://github.com/aframevr/aframe/)
+
+## Getting Started
+
+```
+git clone https://github.com/mozilla/mr-social-client.git
+yarn install
+npm start
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index 8d71ed2bfe7541df34d17b52deabd74308020c52..293a1ad4c183e2ded74b68ec7dc3e27a1779458e 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
 {
-  "name": "social-vr-demo",
+  "name": "mr-social-client",
   "version": "0.0.1",
   "main": "src/index.js",
   "license": "MPL-2.0",
@@ -8,13 +8,16 @@
     "build": "webpack --config webpack.prod.js",
     "prettier": "prettier --write src/**/*.js"
   },
-  "pre-commit": ["prettier"],
+  "pre-commit": [
+    "prettier"
+  ],
   "dependencies": {
     "aframe-input-mapping-component": "https://github.com/fernandojsg/aframe-input-mapping-component",
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
-    "naf-janus-adapter": "^0.1.1",
+    "naf-janus-adapter": "^0.1.4",
     "networked-aframe": "https://github.com/netpro2k/networked-aframe#feature/register-adapter",
-    "pleasejs": "^0.4.2"
+    "pleasejs": "^0.4.2",
+    "query-string": "^5.0.1"
   },
   "devDependencies": {
     "babel-core": "^6.26.0",
diff --git a/public/index.html b/public/index.html
index d2d284960d26a8287c0e4e13a2315cb92ac32f09..5c5d46b4d292baf9145956bcc633c8c80f827155 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,19 +1,20 @@
 <html>
 
 <head>
-    <title>A-Frame Social VR Demo</title>
+    <title>Mozilla Mixed Reality Social Client</title>
     <script src="https://aframe.io/releases/0.7.0/aframe.js"></script>
     <script src="./app.bundle.js"></script>
 </head>
 
 <body>
-    <a-scene networked-scene="
-            adapter: janus;
-            room: 2;
-            serverURL: wss://quander.me:8989;
-            audio: true;
-            debug: true;
-            connectOnLoad: false" mute-mic="eventSrc: a-scene; toggleEvents: action_mute">
+    <a-scene
+        networked-scene="adapter: janus;
+                         room: 2;
+                         serverURL: wss://quander.me:8989;
+                         audio: true;
+                         debug: true;
+                         connectOnLoad: false;"
+        mute-mic="eventSrc: a-scene; toggleEvents: action_mute">
 
         <a-assets>
             <img id="grid" src="assets/grid.png" crossorigin="anonymous" />
@@ -35,11 +36,12 @@
                     matcolor-audio-feedback="objectName: DodecAvatar_Head_0"
                     scale-audio-feedback
                     avatar-customization
+                    personal-space-invader
                 ></a-entity>
             </script>
 
             <script id="hand-template" type="text/html">
-                <a-box class="hand" scale="0.2 0.1 0.3"></a-box>
+                <a-box class="hand" personal-space-invader scale="0.2 0.1 0.3"></a-box>
             </script>
 
             <script id="nametag-template" type="text/html">
@@ -54,7 +56,7 @@
 
         <a-entity id="player-rig" networked wasd-controls snap-rotation="pivotSrc: #head">
             <a-sphere scale="0.1 0.1 0.1"></a-sphere>
-            <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
+            <a-entity id="head" camera="userHeight: 1.6" personal-space-bubble look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
 
             <a-entity id="nametag" networked="template:#nametag-template;showLocalTemplate:false;"></a-entity>
 
@@ -80,4 +82,4 @@
     </script>
 </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js
index ee4b802be1ae2e687c2ee9e3b7869a792bac5faa..2b24abd946d7d8fe2ec35c018c4262f64fd00229 100644
--- a/src/components/audio-feedback.js
+++ b/src/components/audio-feedback.js
@@ -22,12 +22,15 @@ AFRAME.registerComponent("networked-audio-analyser", {
         return NAF.connection.adapter.getMediaStream(ownerId);
       })
       .then(stream => {
+        if (!stream) {
+          return;
+        }
+
         const ctx = THREE.AudioContext.getContext();
         const source = ctx.createMediaStreamSource(stream);
         this.analyser = ctx.createAnalyser();
         this.levels = new Uint8Array(this.analyser.frequencyBinCount);
         source.connect(this.analyser);
-        console.log(source, this.analyser);
       });
   },
 
diff --git a/src/index.js b/src/index.js
index 690a9e69e8fc1fda51aa2abdf31b75a405d546d9..df8b464678bfef1eafe5530b38361f279d541557 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,5 @@
+import queryString from "query-string";
+
 import "networked-aframe";
 import "naf-janus-adapter";
 import "aframe-teleport-controls";
@@ -12,6 +14,8 @@ import "./components/avatar-customization";
 import "./components/mute-state-indicator";
 import "./components/hand-controls-visibility";
 
+import "./systems/personal-space-bubble";
+
 import { generateName } from "./utils";
 
 NAF.schemas.add({
@@ -55,15 +59,26 @@ AFRAME.registerInputMappings({
     }
   }
 });
-window.onSceneLoad = function() {
+
+const promptForName = function() {
   var username = generateName();
   do {
     username = prompt("Choose a username", username);
   } while (!(username && username.length));
+  return username;
+};
+
+const qs = queryString.parse(location.search);
+window.onSceneLoad = function() {
+  const scene = document.querySelector("a-scene");
+
+  if (qs.room && !isNaN(parseInt(qs.room))) {
+    scene.setAttribute("networked-scene", "room", parseInt(qs.room));
+  }
 
-  var player = document.getElementById("player-rig");
-  var myNametag = player.querySelector(".nametag");
+  const username = promptForName(); // promptForName is blocking
+  const myNametag = document.querySelector("#player-rig .nametag");
   myNametag.setAttribute("text", "value", username);
 
-  document.querySelector("a-scene").components["networked-scene"].connect();
+  scene.components["networked-scene"].connect();
 };
diff --git a/src/systems/personal-space-bubble.js b/src/systems/personal-space-bubble.js
new file mode 100644
index 0000000000000000000000000000000000000000..9087ec5a4614c0d80a400e13cca0382bf69236a0
--- /dev/null
+++ b/src/systems/personal-space-bubble.js
@@ -0,0 +1,93 @@
+var invaderPos = new AFRAME.THREE.Vector3();
+var bubblePos = new AFRAME.THREE.Vector3();
+
+AFRAME.registerSystem("personal-space-bubble", {
+  init() {
+    this.invaders = [];
+    this.bubbles = [];
+  },
+
+  registerBubble(el) {
+    this.bubbles.push(el);
+  },
+
+  unregisterBubble(el) {
+    var index = this.bubbles.indexOf(el);
+
+    if (index !== -1) {
+      this.bubbles.splice(index, 1);
+    }
+  },
+
+  registerInvader(el) {
+    var networkedEl = NAF.utils.getNetworkedEntity(el);
+    var owner = NAF.utils.getNetworkOwner(networkedEl);
+
+    if (owner !== NAF.clientId) {
+      this.invaders.push(el);
+    }
+  },
+
+  unregisterInvader(el) {
+    var index = this.invaders.indexOf(el);
+
+    if (index !== -1) {
+      this.invaders.splice(index, 1);
+    }
+  },
+
+  tick() {
+    // Update matrix positions once for each space bubble and space invader
+    for (var i = 0; i < this.bubbles.length; i++) {
+      this.bubbles[i].object3D.updateMatrixWorld(true);
+    }
+
+    for (var i = 0; i < this.invaders.length; i++) {
+      this.invaders[i].object3D.updateMatrixWorld(true);
+    }
+
+    // Loop through all of the space bubbles (usually one)
+    for (var i = 0; i < this.bubbles.length; i++) {
+      var bubble = this.bubbles[i];
+
+      bubblePos.setFromMatrixPosition(bubble.object3D.matrixWorld);
+
+      var radius = bubble.components["personal-space-bubble"].data.radius;
+      var radiusSquared = radius * radius;
+
+      // Hide the invader if inside the bubble
+      for (var j = 0; j < this.invaders.length; j++) {
+        var invader = this.invaders[j];
+
+        invaderPos.setFromMatrixPosition(invader.object3D.matrixWorld);
+
+        var distanceSquared = bubblePos.distanceTo(invaderPos);
+
+        invader.object3D.visible = distanceSquared > radiusSquared;
+      }
+    }
+  }
+});
+
+AFRAME.registerComponent("personal-space-invader", {
+  init() {
+    this.el.sceneEl.systems["personal-space-bubble"].registerInvader(this.el);
+  },
+
+  remove() {
+    this.el.sceneEl.systems["personal-space-bubble"].unregisterInvader(this.el);
+  }
+});
+
+AFRAME.registerComponent("personal-space-bubble", {
+  schema: {
+    radius: { type: "number", default: 0.8 }
+  },
+  init() {
+    this.system.registerBubble(this.el);
+  },
+
+  remove() {
+    this.system.unregisterBubble(this.el);
+  }
+});
diff --git a/yarn.lock b/yarn.lock
index 3608d9e26071cf6a8d1c9b8a142dd022d8004386..bf1721606038eed8522bafbdd999efa9871af59c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1376,6 +1376,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
 
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
 deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -2728,9 +2732,9 @@ multicast-dns@^6.0.1:
     dns-packet "^1.0.1"
     thunky "^0.1.0"
 
-naf-janus-adapter@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.1.1.tgz#9798e11196e38edb04862aebc8213e9345895f8c"
+naf-janus-adapter@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.1.4.tgz#3e6e2022e8f6d5b7f6221fcec8b36a1e1e13be44"
   dependencies:
     minijanus "^0.1.4"
 
@@ -3174,6 +3178,14 @@ qs@~6.4.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
 
+query-string@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88"
+  dependencies:
+    decode-uri-component "^0.2.0"
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
 querystring-es3@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -3672,6 +3684,10 @@ stream-http@^2.3.1:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
+strict-uri-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+
 string-width@^1.0.1, string-width@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"