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/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 52fd9ddd2a9ab2cdc4b54dba0f0093c55dee0d7f..0bc4d32f480927adc71b3d6fee8f06ed1891a60b 100644
--- a/src/room.html
+++ b/src/room.html
@@ -30,7 +30,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>
@@ -48,16 +48,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">
@@ -81,7 +81,7 @@
                             </a-entity>
                         </template>
 
-                        <template selector=".LeftHand">
+                        <template data-selector=".LeftHand">
                             <a-entity personal-space-invader ></a-entity>
                         </template>
 
@@ -147,6 +147,7 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
+            player-info
         >
             <a-entity
                 id="player-camera"
@@ -181,9 +182,13 @@
                 haptic-feedback
             ></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 b09ff896a09145e54bb76ea45f9f659606eaa022..b8e18315139946b4afb258c256f753466ffe79b8 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";
 
@@ -94,7 +96,7 @@ const concurrentLoadDetector = new ConcurrentLoadDetector();
 concurrentLoadDetector.start();
 
 // Always layer in any new default profile bits
-store.update({ profile:  { ...generateDefaultProfile(), ...(store.state.profile || {}) }})
+store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } });
 
 async function shareMedia(audio, video) {
   const constraints = {
@@ -128,15 +130,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);
+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");
-  document.querySelector("a-scene canvas").classList.remove("blurred")
-  scene.setAttribute("networked-scene", "adapter: janus; audio: true; debug: true; connectOnLoad: false;");
+  const playerRig = document.querySelector("#player-rig");
+  const qs = queryString.parse(location.search);
+
+  document.querySelector("a-scene canvas").classList.remove("blurred");
   registerNetworkSchemas();
 
   if (enterInVR) {
@@ -144,12 +152,13 @@ async function enterScene(mediaStream, enterInVR) {
   }
 
   AFRAME.registerInputActions(inGameActions, "default");
-
   document.querySelector("#player-camera").setAttribute("look-controls", "pointerLockEnabled: true;");
 
-  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
   });
@@ -159,12 +168,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);
 
@@ -212,27 +220,31 @@ async function enterScene(mediaStream, enterInVR) {
   }
 }
 
-function onConnect() {
-}
+function onConnect() {}
 
 function mountUI(scene) {
   const qs = queryString.parse(location.search);
-  const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true"
+  const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true";
   let forcedVREntryType = null;
 
   if (qs.vr_entry_type) {
     forcedVREntryType = qs.vr_entry_type;
   }
 
-  const uiRoot = ReactDOM.render(<UIRoot {...{
-    scene,
-    enterScene,
-    exitScene,
-    concurrentLoadDetector,
-    disableAutoExitOnConcurrentLoad,
-    forcedVREntryType,
-    store
-  }} />, document.getElementById("ui-root"));
+  const uiRoot = ReactDOM.render(
+    <UIRoot
+      {...{
+        scene,
+        enterScene,
+        exitScene,
+        concurrentLoadDetector,
+        disableAutoExitOnConcurrentLoad,
+        forcedVREntryType,
+        store
+      }}
+    />,
+    document.getElementById("ui-root")
+  );
 
   getAvailableVREntryTypes().then(availableVREntryTypes => {
     uiRoot.setState({ availableVREntryTypes });