diff --git a/src/hub.html b/src/hub.html
index a4992f194beb69b09b59426394c7ae76623381c5..0abecc4a51f959ecaa182097812518e3bcd3dbf0 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -456,7 +456,9 @@
             id="environment-root"
             nav-mesh-helper
             static-body="shape: none;"
-        ></a-entity>
+        >
+            <a-entity id="environment-scene"/>
+        </a-entity>
 
         <a-entity
         super-spawner="
diff --git a/src/hub.js b/src/hub.js
index 3b648d6aa5570818ff1ad4531f37b836549d95f2..df3083d3ea843c09b2fbfb77c4bb111044f11a1f 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -153,7 +153,46 @@ concurrentLoadDetector.start();
 
 store.init();
 
-function mountUI(scene, props = {}) {
+function getPlatformUnsupportedReason() {
+  if (typeof RTCDataChannelEvent === "undefined") return "no_data_channels";
+  return null;
+}
+
+function pollForSupportAvailability(callback) {
+  const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability");
+  let isSupportAvailable = null;
+
+  const updateIfChanged = () =>
+    fetch(availabilityUrl).then(({ ok }) => {
+      if (isSupportAvailable === ok) return;
+      isSupportAvailable = ok;
+      callback(isSupportAvailable);
+    });
+
+  updateIfChanged();
+  setInterval(updateIfChanged, 30000);
+}
+
+function setupLobbyCamera() {
+  const camera = document.querySelector("#player-camera");
+  const previewCamera = document.querySelector("#environment-scene").object3D.getObjectByName("scene-preview-camera");
+
+  if (previewCamera) {
+    camera.object3D.position.copy(previewCamera.position);
+    camera.object3D.rotation.copy(previewCamera.rotation);
+    camera.object3D.updateMatrix();
+  } else {
+    const cameraPos = camera.object3D.position;
+    camera.object3D.position.set(cameraPos.x, 2.5, cameraPos.z);
+  }
+
+  camera.setAttribute("scene-preview-camera", "positionOnly: true; duration: 60");
+}
+
+let uiProps = {};
+
+function mountUI(props = {}) {
+  const scene = document.querySelector("a-scene");
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
   const forcedVREntryType = qs.get("vr_entry_type");
   const enableScreenSharing = qsTruthy("enable_screen_sharing");
@@ -177,66 +216,138 @@ function mountUI(scene, props = {}) {
   );
 }
 
-const onReady = async () => {
-  const scene = document.querySelector("a-scene");
-  const hubChannel = new HubChannel(store);
-  const linkChannel = new LinkChannel(store);
+function remountUI(props) {
+  uiProps = { ...uiProps, ...props };
+  mountUI(uiProps);
+}
 
-  window.APP.scene = scene;
+async function handleHubChannelJoined(entryManager, data) {
+  const scene = entryManager.scene;
+  const hubChannel = entryManager.hubChannel;
 
-  registerNetworkSchemas();
+  if (NAF.connection.isConnected()) {
+    // Send complete sync on phoenix re-join.
+    NAF.connection.entities.completeSync(null, true);
+    return;
+  }
 
-  let uiProps = { hubChannel, linkChannel };
+  const hub = data.hubs[0];
+  const defaultSpaceTopic = hub.topics[0];
+  const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb");
+  const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle");
+  const sceneUrl = (glbAsset || bundleAsset).src;
+  const hasExtension = /\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl);
+
+  console.log(`Scene URL: ${sceneUrl}`);
+  const environmentScene = document.querySelector("#environment-scene");
+
+  if (glbAsset || hasExtension) {
+    const resolved = await resolveMedia(sceneUrl, false, 0);
+    const gltfEl = document.createElement("a-entity");
+    gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true });
+    gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded"));
+    environmentScene.appendChild(gltfEl);
+  } else {
+    // TODO kill bundles
+    environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
+  }
 
-  mountUI(scene);
+  remountUI({ hubId: hub.hub_id, hubName: hub.name });
 
-  const remountUI = props => {
-    uiProps = { ...uiProps, ...props };
-    mountUI(scene, uiProps);
-  };
+  scene.setAttribute("networked-scene", {
+    room: hub.hub_id,
+    serverURL: process.env.JANUS_SERVER,
+    debug: !!isDebug
+  });
 
-  const pollForSupportAvailability = callback => {
-    let isSupportAvailable = null;
-    const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability");
+  if (isBotMode) {
+    entryManager.enterSceneWhenLoaded(new MediaStream(), false);
+  }
 
-    const updateIfChanged = () => {
-      fetch(availabilityUrl).then(({ ok }) => {
-        if (isSupportAvailable !== ok) {
-          isSupportAvailable = ok;
-          callback(isSupportAvailable);
-        }
-      });
-    };
+  const sendHubDataMessage = function(clientId, dataType, data, reliable) {
+    const event = "naf";
+    const payload = { dataType, data };
 
-    updateIfChanged();
-    setInterval(updateIfChanged, 30000);
-  };
+    if (clientId != null) {
+      payload.clientId = clientId;
+    }
 
-  pollForSupportAvailability(isSupportAvailable => {
-    remountUI({ isSupportAvailable });
-  });
+    if (reliable) {
+      hubChannel.channel.push(event, payload);
+    } else {
+      const topic = hubChannel.channel.topic;
+      const join_ref = hubChannel.channel.joinRef();
+      hubChannel.channel.socket.push({ topic, event, payload, join_ref, ref: null }, false);
+    }
+  };
 
-  const getPlatformUnsupportedReason = () => {
-    if (typeof RTCDataChannelEvent === "undefined") {
-      return "no_data_channels";
+  const connectWhenNetworkedSceneReady = () => {
+    if (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) {
+      setTimeout(connectWhenNetworkedSceneReady, 0);
+      return;
     }
 
-    return null;
+    scene.components["networked-scene"]
+      .connect()
+      .then(() => {
+        NAF.connection.adapter.reliableTransport = (clientId, dataType, data) =>
+          sendHubDataMessage(clientId, dataType, data, true);
+      })
+      .catch(connectError => {
+        // hacky until we get return codes
+        const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i);
+        console.error(connectError);
+        remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" });
+        entryManager.exitScene();
+
+        return;
+      });
   };
 
+  connectWhenNetworkedSceneReady();
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+  const scene = document.querySelector("a-scene");
+  const hubChannel = new HubChannel(store);
   const entryManager = new SceneEntryManager(hubChannel);
-  remountUI({ enterScene: entryManager.enterScene, exitScene: entryManager.exitScene });
+  const linkChannel = new LinkChannel(store);
+
+  window.APP.scene = scene;
+
+  registerNetworkSchemas();
+  mountUI({});
+  remountUI({ hubChannel, linkChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene });
+
+  pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable }));
+
+  document.body.addEventListener("connected", () =>
+    remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 })
+  );
+
+  document.body.addEventListener("clientConnected", () =>
+    remountUI({
+      occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
+    })
+  );
+
+  document.body.addEventListener("clientDisconnected", () =>
+    remountUI({
+      occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
+    })
+  );
 
   const platformUnsupportedReason = getPlatformUnsupportedReason();
 
   if (platformUnsupportedReason) {
-    remountUI({ platformUnsupportedReason: platformUnsupportedReason });
+    remountUI({ platformUnsupportedReason });
     entryManager.exitScene();
     return;
   }
 
   if (qs.get("required_version") && process.env.BUILD_VERSION) {
     const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)"
+
     if (qs.get("required_version") !== buildNumber) {
       remountUI({ roomUnavailableReason: "version_mismatch" });
       setTimeout(() => document.location.reload(), 5000);
@@ -253,25 +364,10 @@ const onReady = async () => {
     }
   });
 
-  const environmentRoot = document.querySelector("#environment-root");
+  document.querySelector("#environment-scene").addEventListener("bundleloaded", () => {
+    remountUI({ environmentSceneLoaded: true });
 
-  const initialEnvironmentEl = document.createElement("a-entity");
-  initialEnvironmentEl.addEventListener("bundleloaded", () => {
-    remountUI({ initialEnvironmentLoaded: true });
-
-    const camera = document.querySelector("#player-camera");
-    const previewCamera = initialEnvironmentEl.object3D.getObjectByName("scene-preview-camera");
-
-    if (previewCamera) {
-      camera.object3D.position.copy(previewCamera.position);
-      camera.object3D.rotation.copy(previewCamera.rotation);
-      camera.object3D.updateMatrix();
-    } else {
-      const cameraPos = camera.object3D.position;
-      camera.object3D.position.set(cameraPos.x, 2.5, cameraPos.z);
-    }
-
-    camera.setAttribute("scene-preview-camera", "positionOnly: true; duration: 60");
+    setupLobbyCamera();
 
     // Replace renderer with a noop renderer to reduce bot resource usage.
     if (isBotMode) {
@@ -279,16 +375,6 @@ const onReady = async () => {
       scene.renderer = { setAnimationLoop: noop, render: noop };
     }
   });
-  environmentRoot.appendChild(initialEnvironmentEl);
-
-  const enterSceneWhenReady = hubId => {
-    const enterSceneImmediately = () => entryManager.enterScene(new MediaStream(), false, hubId);
-    if (scene.hasLoaded) {
-      enterSceneImmediately();
-    } else {
-      scene.addEventListener("loaded", enterSceneImmediately);
-    }
-  };
 
   // Connect to reticulum over phoenix channels to get hub info.
   const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0];
@@ -296,114 +382,13 @@ const onReady = async () => {
 
   const socket = connectToReticulum(isDebug);
   const channel = socket.channel(`hub:${hubId}`, {});
-  let loadedSceneUrl = null;
-
-  // This routine needs to handle re-joins.
-  const handleJoinedHubChannel = async data => {
-    const hub = data.hubs[0];
-    const defaultSpaceTopic = hub.topics[0];
-    const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb");
-    const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle");
-    const sceneUrl = (glbAsset || bundleAsset).src;
-    const hasExtension = /\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl);
-
-    console.log(`Scene URL: ${sceneUrl}`);
-
-    if (glbAsset || hasExtension) {
-      if (loadedSceneUrl !== sceneUrl) {
-        const resolved = await resolveMedia(sceneUrl, false, 0);
-        const gltfEl = document.createElement("a-entity");
-        gltfEl.setAttribute("gltf-model-plus", { src: resolved.raw, useCache: false, inflate: true });
-        gltfEl.addEventListener("model-loaded", () => initialEnvironmentEl.emit("bundleloaded"));
-        initialEnvironmentEl.appendChild(gltfEl);
-        loadedSceneUrl = sceneUrl;
-      }
-    } else {
-      // TODO kill bundles
-      initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
-    }
-
-    remountUI({ hubId: hub.hub_id, hubName: hub.name });
-    hubChannel.setPhoenixChannel(channel);
-
-    scene.setAttribute("networked-scene", {
-      room: hub.hub_id,
-      serverURL: process.env.JANUS_SERVER
-    });
-
-    if (isDebug) {
-      scene.setAttribute("networked-scene", { debug: true });
-    }
-
-    if (isBotMode) enterSceneWhenReady(hub.hub_id);
-
-    if (NAF.connection.adapter) {
-      // Send complete sync on phoenix re-join.
-      NAF.connection.entities.completeSync(null, true);
-    }
-
-    document.body.addEventListener("connected", () => {
-      remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 });
-    });
-
-    document.body.addEventListener("clientConnected", () => {
-      remountUI({
-        occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
-      });
-    });
-
-    document.body.addEventListener("clientDisconnected", () => {
-      remountUI({
-        occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
-      });
-    });
-
-    const sendHubDataMessage = function(clientId, dataType, data, reliable) {
-      const event = "naf";
-      const payload = { dataType, data };
-
-      if (clientId != null) {
-        payload.clientId = clientId;
-      }
-
-      if (reliable) {
-        hubChannel.channel.push(event, payload);
-      } else {
-        const topic = hubChannel.channel.topic;
-        const join_ref = hubChannel.channel.joinRef();
-        hubChannel.channel.socket.push({ topic, event, payload, join_ref, ref: null }, false);
-      }
-    };
-
-    const connectWhenNetworkedSceneReady = () => {
-      if (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) {
-        setTimeout(connectWhenNetworkedSceneReady, 0);
-        return;
-      }
-
-      scene.components["networked-scene"]
-        .connect()
-        .then(() => {
-          NAF.connection.adapter.reliableTransport = (clientId, dataType, data) =>
-            sendHubDataMessage(clientId, dataType, data, true);
-        })
-        .catch(connectError => {
-          // hacky until we get return codes
-          const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i);
-          console.error(connectError);
-          remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" });
-          entryManager.exitScene();
-
-          return;
-        });
-    };
-
-    connectWhenNetworkedSceneReady();
-  };
 
   channel
     .join()
-    .receive("ok", handleJoinedHubChannel)
+    .receive("ok", async data => {
+      hubChannel.setPhoenixChannel(channel);
+      await handleHubChannelJoined(entryManager, data);
+    })
     .receive("error", res => {
       if (res.reason === "closed") {
         entryManager.exitScene();
@@ -414,12 +399,9 @@ const onReady = async () => {
     });
 
   channel.on("naf", data => {
-    if (NAF.connection.adapter) {
-      NAF.connection.adapter.onData(data);
-    }
+    if (!NAF.connection.adapter) return;
+    NAF.connection.adapter.onData(data);
   });
 
   linkChannel.setSocket(socket);
-};
-
-document.addEventListener("DOMContentLoaded", onReady);
+});
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index ddc6608fe643df4a34a95d86d081ae0e5e7ca89f..c56ed0b0c663a5961c8bf2631d9fabc196453f9a 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -75,7 +75,7 @@ class UIRoot extends Component {
     linkChannel: PropTypes.object,
     showProfileEntry: PropTypes.bool,
     availableVREntryTypes: PropTypes.object,
-    initialEnvironmentLoaded: PropTypes.bool,
+    environmentSceneLoaded: PropTypes.bool,
     roomUnavailableReason: PropTypes.string,
     platformUnsupportedReason: PropTypes.string,
     hubId: PropTypes.string,
@@ -639,7 +639,7 @@ class UIRoot extends Component {
       );
     }
 
-    if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.hubId) {
+    if (!this.props.environmentSceneLoaded || !this.props.availableVREntryTypes || !this.props.hubId) {
       return (
         <IntlProvider locale={lang} messages={messages}>
           <div className="loading-panel">
diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js
index b82fd1deef23bfae83faf4f77f8653a9a37f0d5e..ce67c4820f2e47eb3317ee25e4916d8eb73b4cf1 100644
--- a/src/scene-entry-manager.js
+++ b/src/scene-entry-manager.js
@@ -75,6 +75,16 @@ export default class SceneEntryManager {
     this.spawnAvatar();
   };
 
+  enterSceneWhenLoaded = (mediaStream, enterInVR) => {
+    const enterSceneImmediately = () => this.enterScene(mediaStream, enterInVR);
+
+    if (this.scene.hasLoaded) {
+      enterSceneImmediately();
+    } else {
+      this.scene.addEventListener("loaded", enterSceneImmediately);
+    }
+  };
+
   exitScene = () => {
     if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
       NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());