diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..9c72e3d2fc170d6b0782d741eca441a62780daad
Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar.glb differ
diff --git a/src/components/debug.js b/src/components/debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..77202f03295be677b9156e1ff16f11100ab19351
--- /dev/null
+++ b/src/components/debug.js
@@ -0,0 +1,25 @@
+AFRAME.registerComponent("lifecycle-checker", {
+  schema: {
+    tick: { default: false }
+  },
+  init: function() {
+    console.log("init", this.el);
+  },
+  update: function() {
+    console.log("update", this.el);
+  },
+  tick: function() {
+    if (this.data.tick) {
+      console.log("tick", this.el);
+    }
+  },
+  remove: function() {
+    console.log("remove", this.el);
+  },
+  pause: function() {
+    console.log("pause", this.el);
+  },
+  play: function() {
+    console.log("play", this.el);
+  }
+});
diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js
index 3b4f323df72d83cfbe0d03ab00094318485eed22..0a1990bd8ede46f4e5bbdde5cfced182ed4f5a3d 100644
--- a/src/components/ik-controller.js
+++ b/src/components/ik-controller.js
@@ -7,29 +7,27 @@ AFRAME.registerComponent("ik-root", {
     rightController: { type: "string", default: ".right-controller" }
   },
   update(oldData) {
-    let updated = false;
-
     if (this.data.camera !== oldData.camera) {
       this.camera = this.el.querySelector(this.data.camera);
-      updated = true;
     }
 
     if (this.data.leftController !== oldData.leftController) {
       this.leftController = this.el.querySelector(this.data.leftController);
-      updated = true;
     }
 
     if (this.data.rightController !== oldData.rightController) {
       this.rightController = this.el.querySelector(this.data.rightController);
-      updated = true;
-    }
-
-    if (updated) {
-      this.el.querySelector("[ik-controller]").components["ik-controller"].updateIkRoot(this);
     }
   }
 });
 
+function findIKRoot(entity) {
+  while (entity && !(entity.components && entity.components["ik-root"])) {
+    entity = entity.parentNode;
+  }
+  return entity && entity.components["ik-root"];
+}
+
 AFRAME.registerComponent("ik-controller", {
   schema: {
     leftEye: { type: "string", default: ".LeftEye" },
@@ -65,6 +63,8 @@ AFRAME.registerComponent("ik-controller", {
     this.rootToChest = new Matrix4();
     this.invRootToChest = new Matrix4();
 
+    this.ikRoot = findIKRoot(this.el);
+
     this.hands = {
       left: {
         lastVisible: true,
@@ -124,10 +124,6 @@ AFRAME.registerComponent("ik-controller", {
       .negate();
   },
 
-  updateIkRoot(ikRoot) {
-    this.ikRoot = ikRoot;
-  },
-
   tick(time, dt) {
     if (!this.ikRoot) {
       return;
diff --git a/src/components/player-info.js b/src/components/player-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cb265a67b9c67ac26510d6b8a03efb59703c931
--- /dev/null
+++ b/src/components/player-info.js
@@ -0,0 +1,31 @@
+AFRAME.registerComponent("player-info", {
+  schema: {
+    displayName: { type: "string" },
+    avatar: { type: "string" }
+  },
+  init() {
+    this.applyProperties = this.applyProperties.bind(this);
+  },
+  play() {
+    this.el.addEventListener("model-loaded", this.applyProperties);
+  },
+  pause() {
+    this.el.removeEventListener("model-loaded", this.applyProperties);
+  },
+  update(oldProps) {
+    this.applyProperties();
+  },
+  applyProperties() {
+    const nametagEl = this.el.querySelector(".nametag");
+    if (this.data.displayName && nametagEl) {
+      nametagEl.setAttribute("text", {
+        value: this.data.displayName
+      });
+    }
+
+    const modelEl = this.el.querySelector(".model");
+    if (this.data.avatar && modelEl) {
+      modelEl.setAttribute("src", this.data.avatar);
+    }
+  }
+});
diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js
index a500efb229d044e465925f0754443520503ba886..e498a1acc2ad9b99c9131134bd17560bee1af531 100644
--- a/src/components/super-cursor.js
+++ b/src/components/super-cursor.js
@@ -20,7 +20,6 @@ AFRAME.registerComponent("super-cursor", {
     this.direction = new THREE.Vector3();
     this.point = new THREE.Vector3();
     this.mousePos = new THREE.Vector2();
-    this.mouseDown = false;
 
     this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
 
@@ -60,7 +59,6 @@ AFRAME.registerComponent("super-cursor", {
     }
 
     this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
-    let isIntersecting = false;
 
     const camera = this.data.camera.components.camera.camera;
     const raycaster = this.el.components.raycaster.raycaster;
@@ -69,15 +67,13 @@ AFRAME.registerComponent("super-cursor", {
     this.direction = raycaster.ray.direction;
     this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
 
-    let className = null;
+    let intersection = null;
 
     if (!this.isGrabbing) {
       const intersections = this.el.components.raycaster.intersections;
       if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
-        isIntersecting = true;
-        className = intersections[0].object.el.className;
-        this.point = intersections[0].point;
-        this.data.cursor.object3D.position.copy(this.point);
+        intersection = intersections[0];
+        this.data.cursor.object3D.position.copy(intersection.point);
         this.currentDistance = intersections[0].distance;
         this.currentDistanceMod = 0;
       } else {
@@ -85,7 +81,7 @@ AFRAME.registerComponent("super-cursor", {
       }
     }
 
-    if (this.isGrabbing || !isIntersecting) {
+    if (this.isGrabbing || !intersection) {
       const distance = Math.min(
         Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod),
         this.data.maxDistance
@@ -96,7 +92,7 @@ AFRAME.registerComponent("super-cursor", {
       this.data.cursor.object3D.position.copy(this.point);
     }
 
-    this.isInteractable = isIntersecting && className === "interactable";
+    this.isInteractable = intersection && intersection.object.el.className === "interactable";
 
     if ((this.isGrabbing || this.isInteractable) && !this.wasIntersecting) {
       this.wasIntersecting = true;
@@ -108,7 +104,6 @@ AFRAME.registerComponent("super-cursor", {
   },
 
   _handleMouseDown: function(e) {
-    this.mouseDown = true;
     if (this.isInteractable) {
       const lookControls = this.data.camera.components["look-controls"];
       lookControls.pause();
@@ -121,7 +116,6 @@ AFRAME.registerComponent("super-cursor", {
   },
 
   _handleMouseUp: function(e) {
-    this.mouseDown = false;
     const lookControls = this.data.camera.components["look-controls"];
     lookControls.play();
     this.data.cursor.emit("action_release", {});
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index 3c9c07031eecbf21069569b0f2738fe48afcea25..e88f7a76bf6ee4218f37997d9497e576b3bed55a 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -22,19 +22,21 @@ AFRAME.AGLTFEntity = {
 // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
 function cloneGltf(gltf) {
-  const clone = {
-    animations: gltf.animations,
-    scene: gltf.scene.clone(true)
-  };
-
   const skinnedMeshes = {};
-
   gltf.scene.traverse(node => {
+    if (!node.name) {
+      node.name = node.uuid;
+    }
     if (node.isSkinnedMesh) {
       skinnedMeshes[node.name] = node;
     }
   });
 
+  const clone = {
+    animations: gltf.animations,
+    scene: gltf.scene.clone(true)
+  };
+
   const cloneBones = {};
   const cloneSkinnedMeshes = {};
 
@@ -68,18 +70,24 @@ function cloneGltf(gltf) {
   return clone;
 }
 
-const inflateEntities = function(classPrefix, parentEl, node) {
+const inflateEntities = function(parentEl, node) {
   // setObject3D mutates the node's parent, so we have to copy
   const children = node.children.slice(0);
 
   const el = document.createElement("a-entity");
 
   // Remove invalid CSS class name characters.
-  const className = node.name.replace(/[^\w-]/g, "");
-  el.classList.add(classPrefix + className);
+  const className = (node.name || node.uuid).replace(/[^\w-]/g, "");
+  el.classList.add(className);
   parentEl.appendChild(el);
 
-  // Copy over transform to the THREE.Group and reset the actual transform of the Object3D
+  // AFRAME rotation component expects rotations in YXZ, convert it
+  if (node.rotation.order !== "YXZ") {
+    node.rotation.setFromQuaternion(node.quaternion, "YXZ");
+  }
+
+  // Copy over the object's transform to the THREE.Group and reset the actual transform of the Object3D
+  // all updates to the object should be done through the THREE.Group wrapper
   el.setAttribute("position", {
     x: node.position.x,
     y: node.position.y,
@@ -95,10 +103,8 @@ const inflateEntities = function(classPrefix, parentEl, node) {
     y: node.scale.y,
     z: node.scale.z
   });
-
-  node.position.set(0, 0, 0);
-  node.rotation.set(0, 0, 0);
-  node.scale.set(1, 1, 1);
+  node.matrixAutoUpdate = false;
+  node.matrix.identity();
 
   el.setObject3D(node.type.toLowerCase(), node);
 
@@ -116,7 +122,6 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   const entityComponents = node.userData.components;
-
   if (entityComponents) {
     for (const prop in entityComponents) {
       if (entityComponents.hasOwnProperty(prop)) {
@@ -130,116 +135,184 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   children.forEach(childNode => {
-    inflateEntities(classPrefix, el, childNode);
+    inflateEntities(el, childNode);
   });
-};
 
-function attachTemplate(templateEl) {
-  const selector = templateEl.getAttribute("data-selector");
-  const targetEls = templateEl.parentNode.querySelectorAll(selector);
-  const clone = document.importNode(templateEl.content, true);
-  const templateRoot = clone.firstElementChild;
+  return el;
+};
 
+function attachTemplate(root, { selector, templateRoot }) {
+  const targetEls = root.querySelectorAll(selector);
   for (const el of targetEls) {
+    const root = templateRoot.cloneNode(true);
     // Merge root element attributes with the target element
-    for (const { name, value } of templateRoot.attributes) {
+    for (const { name, value } of root.attributes) {
       el.setAttribute(name, value);
     }
 
     // Append all child elements
-    for (const child of templateRoot.children) {
-      el.appendChild(document.importNode(child, true));
+    for (const child of root.children) {
+      el.appendChild(child);
     }
   }
 }
 
+function nextTick() {
+  return new Promise(resolve => {
+    setTimeout(resolve, 0);
+  });
+}
+
+function cachedLoadGLTF(src, onProgress) {
+  return new Promise((resolve, reject) => {
+    // Load the gltf model from the cache if it exists.
+    if (GLTFCache[src]) {
+      // Use a cloned copy of the cached model.
+      resolve(cloneGltf(GLTFCache[src]));
+    } else {
+      // Otherwise load the new gltf model.
+      new THREE.GLTFLoader().load(
+        src,
+        model => {
+          if (!GLTFCache[src]) {
+            // Store a cloned copy of the gltf model.
+            GLTFCache[src] = cloneGltf(model);
+          }
+          resolve(model);
+        },
+        onProgress,
+        reject
+      );
+    }
+  });
+}
+
 AFRAME.registerElement("a-gltf-entity", {
   prototype: Object.create(AFRAME.AEntity.prototype, {
     load: {
-      value() {
+      async value() {
         if (this.hasLoaded || !this.parentEl) {
           return;
         }
 
-        // Get the src url.
-        let src = this.getAttribute("src");
-
-        // If the src attribute is a selector, get the url from the asset item.
-        if (src.charAt(0) === "#") {
-          const assetEl = document.getElementById(src.substring(1));
-
-          const fallbackSrc = assetEl.getAttribute("src");
-          const highSrc = assetEl.getAttribute("high-src");
-          const lowSrc = assetEl.getAttribute("low-src");
+        // The code above and below this are from AEntity.prototype.load, we need to monkeypatch in gltf loading mid function
+        this.loadTemplates();
+        await this.applySrc(this.getAttribute("src"));
+        //
 
-          if (highSrc && window.APP.quality === "high") {
-            src = highSrc;
-          } else if (lowSrc && window.APP.quality === "low") {
-            src = lowSrc;
-          } else {
-            src = fallbackSrc;
+        AFRAME.ANode.prototype.load.call(this, () => {
+          // Check if entity was detached while it was waiting to load.
+          if (!this.parentEl) {
+            return;
           }
-        }
 
-        const onLoad = gltfModel => {
-          if (!GLTFCache[src]) {
-            // Store a cloned copy of the gltf model.
-            GLTFCache[src] = cloneGltf(gltfModel);
+          this.updateComponents();
+          if (this.isScene || this.parentEl.isPlaying) {
+            this.play();
           }
+        });
+      }
+    },
 
-          this.model = gltfModel.scene || gltfModel.scenes[0];
-          this.model.animations = gltfModel.animations;
+    loadTemplates: {
+      value() {
+        this.templates = [];
+        this.querySelectorAll(":scope > template").forEach(templateEl =>
+          this.templates.push({
+            selector: templateEl.getAttribute("data-selector"),
+            templateRoot: document.importNode(templateEl.content.firstElementChild, true)
+          })
+        );
+      }
+    },
+
+    applySrc: {
+      async value(src) {
+        try {
+          // If the src attribute is a selector, get the url from the asset item.
+          if (src && src.charAt(0) === "#") {
+            const assetEl = document.getElementById(src.substring(1));
+
+            const fallbackSrc = assetEl.getAttribute("src");
+            const highSrc = assetEl.getAttribute("high-src");
+            const lowSrc = assetEl.getAttribute("low-src");
+
+            if (highSrc && window.APP.quality === "high") {
+              src = highSrc;
+            } else if (lowSrc && window.APP.quality === "low") {
+              src = lowSrc;
+            } else {
+              src = fallbackSrc;
+            }
+          }
 
-          this.setObject3D("mesh", this.model);
-          this.emit("model-loaded", { format: "gltf", model: this.model });
+          if (src === this.lastSrc) return;
+          this.lastSrc = src;
 
-          if (this.getAttribute("inflate")) {
-            inflate(this.model, finalizeLoad);
-          } else {
-            finalizeLoad();
+          if (!src) {
+            if (this.inflatedEl) {
+              console.warn("gltf-entity set to an empty source, unloading inflated model.");
+              this.removeInflatedEl();
+            }
+            return;
           }
-        };
 
-        const inflate = (model, callback) => {
-          inflateEntities("", this, model);
-          this.querySelectorAll(":scope > template").forEach(attachTemplate);
+          const model = await cachedLoadGLTF(src);
 
-          // Wait one tick for the appended custom elements to be connected before calling finalizeLoad
-          setTimeout(callback, 0);
-        };
+          // If we started loading something else already
+          // TODO: there should be a way to cancel loading instead
+          if (src != this.lastSrc) return;
 
-        const finalizeLoad = () => {
-          AFRAME.ANode.prototype.load.call(this, () => {
-            // Check if entity was detached while it was waiting to load.
-            if (!this.parentEl) {
-              return;
-            }
+          // If we had inflated something already before, clean that up
+          this.removeInflatedEl();
 
-            this.updateComponents();
-            if (this.isScene || this.parentEl.isPlaying) {
-              this.play();
-            }
-          });
-        };
+          this.model = model.scene || model.scenes[0];
+          this.model.animations = model.animations;
+
+          this.setObject3D("mesh", this.model);
 
-        // Load the gltf model from the cache if it exists.
-        const gltf = GLTFCache[src];
+          if (this.getAttribute("inflate")) {
+            this.inflatedEl = inflateEntities(this, this.model);
+            // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more
+            // Wait one tick for the appended custom elements to be connected before attaching templates
+            await nextTick();
+            if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this
+            this.templates.forEach(attachTemplate.bind(null, this));
+          }
 
-        if (gltf) {
-          // Use a cloned copy of the cached model.
-          const clonedGltf = cloneGltf(gltf);
-          onLoad(clonedGltf);
-          return;
+          this.emit("model-loaded", { format: "gltf", model: this.model });
+        } catch (e) {
+          const message = (e && e.message) || "Failed to load glTF model";
+          console.error(message);
+          this.emit("model-error", { format: "gltf", src });
+        }
+      }
+    },
+
+    removeInflatedEl: {
+      value() {
+        if (this.inflatedEl) {
+          this.inflatedEl.parentNode.removeChild(this.inflatedEl);
+          delete this.inflatedEl;
         }
+      }
+    },
 
-        // Otherwise load the new gltf model.
-        new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => {
-          // On glTF load error
+    attributeChangedCallback: {
+      value(attr, oldVal, newVal) {
+        if (attr === "src") {
+          this.applySrc(newVal);
+        }
+        AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal);
+      }
+    },
 
-          const message = error && error.message ? error.message : "Failed to load glTF model";
-          console.warn(message);
-          this.emit("model-error", { format: "gltf", src });
-        });
+    setAttribute: {
+      value(attr, arg1, arg2) {
+        if (attr === "src") {
+          this.applySrc(arg1);
+        }
+        AFRAME.AEntity.prototype.setAttribute.call(this, attr, arg1, arg2);
       }
     }
   })
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 4632a5933807b1e97132c726fcc335afa6b68964..54cc0b63ba40efed17baf43c0d3554b1ea2fa4bc 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -5,6 +5,7 @@ function registerNetworkSchemas() {
       "position",
       "rotation",
       "scale",
+      "player-info",
       {
         selector: ".camera",
         component: "position"
@@ -36,11 +37,6 @@ function registerNetworkSchemas() {
       {
         selector: ".right-controller",
         component: "visible"
-      },
-      {
-        selector: ".nametag",
-        component: "text",
-        property: "value"
       }
     ]
   });
diff --git a/src/room.html b/src/room.html
index 39fc77bbc281257dd59b03f9ec38e7488552b8fe..3d8c1a5f96c35cbc0576189a26325966ddc98eea 100644
--- a/src/room.html
+++ b/src/room.html
@@ -35,7 +35,7 @@
                 high-src="./assets/avatars/BotDefault_Avatar.glb"
                 low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
             ></a-progressive-asset>
-
+            <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item>
             <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item>
 
             <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item>
@@ -53,16 +53,16 @@
             </template>
 
             <template id="remote-avatar-template">
-                <a-entity ik-root>
+                <a-entity ik-root player-info>
                     <a-entity class="camera"></a-entity>
 
                     <a-entity class="left-controller"></a-entity>
 
                     <a-entity class="right-controller"></a-entity>
 
-                    <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+                    <a-gltf-entity class="model" inflate="true">
                         <template data-selector=".RootScene">
-                            <a-entity animation-mixer ></a-entity>
+                            <a-entity ik-controller animation-mixer></a-entity>
                         </template>
 
                         <template data-selector=".Neck">
@@ -86,7 +86,7 @@
                             </a-entity>
                         </template>
 
-                        <template selector=".LeftHand">
+                        <template data-selector=".LeftHand">
                             <a-entity personal-space-invader ></a-entity>
                         </template>
 
@@ -156,6 +156,7 @@
             ik-root
             app-mode-toggle-playing__character-controller="mode: hud; invert: true;"
             app-mode-toggle-playing__wasd-to-analog2d="mode: hud; invert: true;"
+            player-info
         >
 
             <a-entity
@@ -209,9 +210,13 @@
                 app-mode-toggle-attribute__line="mode: hud; property: visible;"
             ></a-entity>
 
-            <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+            <a-gltf-entity class="model" inflate="true">
                 <template data-selector=".RootScene">
-                    <a-entity animation-mixer animated-robot-hands ></a-entity>
+                    <a-entity
+                        ik-controller 
+                        animated-robot-hands
+                        animation-mixer
+                    ></a-entity>
                 </template>
 
                 <template data-selector=".Neck">
diff --git a/src/room.js b/src/room.js
index f71c6598dd53738c3e941f720ea1fafe835c91e8..b8a954920a2f6e1a2fb24c2378af7e2ba7dec68e 100644
--- a/src/room.js
+++ b/src/room.js
@@ -37,6 +37,8 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./components/animated-robot-hands";
 import "./components/hide-when-quality";
+import "./components/player-info";
+import "./components/debug";
 import "./components/animation-mixer";
 import "./components/loop-animation";
 
@@ -133,16 +135,21 @@ async function exitScene() {
   document.body.removeChild(scene);
 }
 
-function setNameTagFromStore() {
-  const myNametag = document.querySelector("#player-rig .nametag");
-  myNametag.setAttribute("text", "value", store.state.profile.display_name);
-  document.querySelector("a-scene").emit("username-changed", { username: store.state.profile.display_name });
+function updatePlayerInfoFromStore() {
+  const qs = queryString.parse(location.search);
+  const playerRig = document.querySelector("#player-rig");
+  playerRig.setAttribute("player-info", {
+    displayName: store.state.profile.display_name,
+    avatar: qs.avatar || "#bot-skinned-mesh"
+  });
 }
 
 async function enterScene(mediaStream, enterInVR) {
   const scene = document.querySelector("a-scene");
+  const playerRig = document.querySelector("#player-rig");
+  const qs = queryString.parse(location.search);
+
   document.querySelector("a-scene canvas").classList.remove("blurred");
-  scene.setAttribute("networked-scene", "adapter: janus; audio: true; debug: true; connectOnLoad: false;");
   registerNetworkSchemas();
 
   if (enterInVR) {
@@ -151,9 +158,11 @@ async function enterScene(mediaStream, enterInVR) {
 
   AFRAME.registerInputActions(inGameActions, "default");
 
-  const qs = queryString.parse(location.search);
-
   scene.setAttribute("networked-scene", {
+    adapter: "janus",
+    audio: true,
+    debug: true,
+    connectOnLoad: false,
     room: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1,
     serverURL: process.env.JANUS_SERVER
   });
@@ -163,12 +172,11 @@ async function enterScene(mediaStream, enterInVR) {
   }
 
   if (isMobile || qs.mobile) {
-    const playerRig = document.querySelector("#player-rig");
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
 
-  setNameTagFromStore();
-  store.addEventListener("statechanged", setNameTagFromStore);
+  updatePlayerInfoFromStore();
+  store.addEventListener("statechanged", updatePlayerInfoFromStore);
 
   const avatarScale = parseInt(qs.avatarScale, 10);