diff --git a/scripts/default.env b/scripts/default.env
index a1186bbf1f20d84c6883a36bedea1e6ac88e4e7c..b5dbe2c7c9ec3b22eba8d36f07c2fbd700bf0cb0 100644
--- a/scripts/default.env
+++ b/scripts/default.env
@@ -3,3 +3,4 @@
 ORIGIN_TRIAL_TOKEN="ArEZ0vY0uMo3pj+oY8Up4u4Hy8QolJwKxG4/2WRhSPnTZRrviiGhzP6/y72nBdsIhdEyoundxqg//KLbs2vGnQoAAABkeyJvcmlnaW4iOiJodHRwczovL3JldGljdWx1bS5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViVlIxLjFNNjIiLCJleHBpcnkiOjE1MjYzNDg2MjEsImlzU3ViZG9tYWluIjp0cnVlfQ=="
 ORIGIN_TRIAL_EXPIRES="2018-05-15"
 JANUS_SERVER="wss://prod-janus.reticulum.io"
+DEV_RETICULUM_SERVER="dev.reticulum.io"
diff --git a/src/assets/stylesheets/exited.scss b/src/assets/stylesheets/exited.scss
index 693d6d38798705979478930f0b175ea57aa6e183..e54066078bad963cb7fa37069be6bdbab05a56ab 100644
--- a/src/assets/stylesheets/exited.scss
+++ b/src/assets/stylesheets/exited.scss
@@ -10,11 +10,18 @@
   flex-direction: column;
 
   &__title {
+    @extend %default-font;
     font-size: 1.2em;
   }
 
   &__subtitle {
-    font-size: 0.8em;
+    @extend %default-font;
+    padding: 12px;
+    text-align: center;
+    
+    a {
+      color: $light-text;
+    }
   }
 }
 
diff --git a/src/assets/stylesheets/hub-create.scss b/src/assets/stylesheets/hub-create.scss
index 636ab5d656725522d5a407d93c8a1d92361d61ca..31e49358ec78660c6bd9b10b1ce321bc6225c876 100644
--- a/src/assets/stylesheets/hub-create.scss
+++ b/src/assets/stylesheets/hub-create.scss
@@ -192,9 +192,13 @@
 	      font-size: 1.4em;
 
 	      @media (max-width: 520px) {
-		display: none;
+		      display: none;
 	      }
-	    }
+      }
+
+      a {
+        pointer-events: all;
+      }
 	  }
 	}
 
diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss
index 38833657208e9a58a19ef8b10add9e8a5ceaa25d..b09114616dd142d6272899ea88ca70564511f741 100644
--- a/src/assets/stylesheets/index.scss
+++ b/src/assets/stylesheets/index.scss
@@ -218,6 +218,10 @@ body {
 
     &__bottom {
       margin-top: 4px;
+
+      a {
+	color: $grey-text;
+      }
     }
   }
 }
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 2ad5c1cf95deb131b7b8b4aa02add9f63c11cc7f..6d29a6306cfb0aa0438a456099ab93f986704fab 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -32,7 +32,9 @@
     "audio.granted-title": "Mic permissions granted",
     "audio.granted-subtitle": "You can still mute yourself in-game",
     "audio.granted-next": "NEXT",
-    "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.",
+    "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.",
+    "exit.subtitle.full": "This room is full, please try again later.",
+    "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.",
     "autoexit.title": "Auto-ending session in ",
     "autoexit.title_units": " seconds",
     "autoexit.subtitle": "You have started another session.",
diff --git a/src/assets/video/home.webm b/src/assets/video/home.webm
old mode 100644
new mode 100755
index d7156cad9530c12a8b0b3f31332cfebda0f4cea9..e845e128ca83848e77a1884a944e543bc7179afe
Binary files a/src/assets/video/home.webm and b/src/assets/video/home.webm differ
diff --git a/src/hub.js b/src/hub.js
index cf096150c926e89d4999d87cd0ff34210dc73cd3..3d0b85a4b7733864cd2915cf65944c5a9f5eb71b 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -112,7 +112,6 @@ AFRAME.registerInputMappings(inputConfig, true);
 
 const store = new Store();
 const concurrentLoadDetector = new ConcurrentLoadDetector();
-const hubChannel = new HubChannel(store);
 
 concurrentLoadDetector.start();
 
@@ -124,111 +123,6 @@ if (!store.state.profile.has_changed_name) {
   store.update({ profile: { display_name: generateRandomName() } });
 }
 
-async function exitScene() {
-  if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
-    NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
-  }
-  hubChannel.disconnect();
-  const scene = document.querySelector("a-scene");
-  scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
-  document.body.removeChild(scene);
-}
-
-function applyProfileFromStore(playerRig) {
-  const displayName = store.state.profile.display_name;
-  playerRig.setAttribute("player-info", {
-    displayName,
-    avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault")
-  });
-  document.querySelector("a-scene").emit("username-changed", { username: displayName });
-}
-
-async function enterScene(mediaStream, enterInVR, janusRoomId) {
-  const scene = document.querySelector("a-scene");
-  const playerRig = document.querySelector("#player-rig");
-  document.querySelector("a-scene canvas").classList.remove("blurred");
-  scene.render();
-
-  scene.setAttribute("stats-plus", false);
-
-  if (enterInVR) {
-    scene.enterVR();
-  }
-
-  AFRAME.registerInputActions(inGameActions, "default");
-
-  document.querySelector("#player-camera").setAttribute("look-controls", "");
-
-  scene.setAttribute("networked-scene", {
-    room: janusRoomId,
-    serverURL: process.env.JANUS_SERVER
-  });
-
-  if (isMobile || qsTruthy("mobile")) {
-    playerRig.setAttribute("virtual-gamepad-controls", {});
-  }
-
-  const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig);
-  applyProfileOnPlayerRig();
-  store.addEventListener("statechanged", applyProfileOnPlayerRig);
-
-  const avatarScale = parseInt(qs.avatar_scale, 10);
-
-  if (avatarScale) {
-    playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale });
-  }
-
-  const videoTracks = mediaStream.getVideoTracks();
-  let sharingScreen = videoTracks.length > 0;
-
-  const screenEntityId = `${NAF.clientId}-screen`;
-  let screenEntity = document.getElementById(screenEntityId);
-
-  scene.addEventListener("action_share_screen", () => {
-    sharingScreen = !sharingScreen;
-    if (sharingScreen) {
-      for (const track of videoTracks) {
-        mediaStream.addTrack(track);
-      }
-    } else {
-      for (const track of mediaStream.getVideoTracks()) {
-        mediaStream.removeTrack(track);
-      }
-    }
-    NAF.connection.adapter.setLocalMediaStream(mediaStream);
-    screenEntity.setAttribute("visible", sharingScreen);
-  });
-
-  if (!qsTruthy("offline")) {
-    document.body.addEventListener("connected", () => {
-      hubChannel.sendEntryEvent().then(() => {
-        store.update({ lastEnteredAt: moment().toJSON() });
-      });
-    });
-
-    scene.components["networked-scene"].connect();
-
-    if (mediaStream) {
-      NAF.connection.adapter.setLocalMediaStream(mediaStream);
-
-      if (screenEntity) {
-        screenEntity.setAttribute("visible", sharingScreen);
-      } else if (sharingScreen) {
-        const sceneEl = document.querySelector("a-scene");
-        screenEntity = document.createElement("a-entity");
-        screenEntity.id = screenEntityId;
-        screenEntity.setAttribute("offset-relative-to", {
-          target: "#player-camera",
-          offset: "0 0 -2",
-          on: "action_share_screen"
-        });
-        screenEntity.setAttribute("networked", { template: "#video-template" });
-        sceneEl.appendChild(screenEntity);
-      }
-    }
-  }
-}
-
 function mountUI(scene, props = {}) {
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
   const forcedVREntryType = qs.vr_entry_type || null;
@@ -240,8 +134,6 @@ function mountUI(scene, props = {}) {
     <UIRoot
       {...{
         scene,
-        enterScene,
-        exitScene,
         concurrentLoadDetector,
         disableAutoExitOnConcurrentLoad,
         forcedVREntryType,
@@ -258,19 +150,135 @@ function mountUI(scene, props = {}) {
 
 const onReady = async () => {
   const scene = document.querySelector("a-scene");
+  const hubChannel = new HubChannel(store);
+
   document.querySelector("a-scene canvas").classList.add("blurred");
   window.APP.scene = scene;
 
   registerNetworkSchemas();
 
+  let uiProps = {};
+
   mountUI(scene);
 
-  let modifiedProps = {};
   const remountUI = props => {
-    modifiedProps = { ...modifiedProps, ...props };
-    mountUI(scene, modifiedProps);
+    uiProps = { ...uiProps, ...props };
+    mountUI(scene, uiProps);
+  };
+
+  const applyProfileFromStore = playerRig => {
+    const displayName = store.state.profile.display_name;
+    playerRig.setAttribute("player-info", {
+      displayName,
+      avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault")
+    });
+    document.querySelector("a-scene").emit("username-changed", { username: displayName });
+  };
+
+  const exitScene = () => {
+    hubChannel.disconnect();
+    const scene = document.querySelector("a-scene");
+    scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
+    document.body.removeChild(scene);
+  };
+
+  const enterScene = async (mediaStream, enterInVR, janusRoomId) => {
+    const scene = document.querySelector("a-scene");
+    const playerRig = document.querySelector("#player-rig");
+    document.querySelector("a-scene canvas").classList.remove("blurred");
+    scene.render();
+
+    if (enterInVR) {
+      scene.enterVR();
+    }
+
+    AFRAME.registerInputActions(inGameActions, "default");
+
+    document.querySelector("#player-camera").setAttribute("look-controls", "");
+
+    scene.setAttribute("networked-scene", {
+      room: janusRoomId,
+      serverURL: process.env.JANUS_SERVER
+    });
+
+    if (!qsTruthy("no_stats")) {
+      scene.setAttribute("stats", true);
+    }
+
+    if (isMobile || qsTruthy("mobile")) {
+      playerRig.setAttribute("virtual-gamepad-controls", {});
+    }
+
+    const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig);
+    applyProfileOnPlayerRig();
+    store.addEventListener("statechanged", applyProfileOnPlayerRig);
+
+    const avatarScale = parseInt(qs.avatar_scale, 10);
+
+    if (avatarScale) {
+      playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale });
+    }
+
+    const videoTracks = mediaStream.getVideoTracks();
+    let sharingScreen = videoTracks.length > 0;
+
+    const screenEntityId = `${NAF.clientId}-screen`;
+    let screenEntity = document.getElementById(screenEntityId);
+
+    scene.addEventListener("action_share_screen", () => {
+      sharingScreen = !sharingScreen;
+      if (sharingScreen) {
+        for (const track of videoTracks) {
+          mediaStream.addTrack(track);
+        }
+      } else {
+        for (const track of mediaStream.getVideoTracks()) {
+          mediaStream.removeTrack(track);
+        }
+      }
+      NAF.connection.adapter.setLocalMediaStream(mediaStream);
+      screenEntity.setAttribute("visible", sharingScreen);
+    });
+
+    if (!qsTruthy("offline")) {
+      document.body.addEventListener("connected", () => {
+        hubChannel.sendEntryEvent().then(() => {
+          store.update({ lastEnteredAt: moment().toJSON() });
+        });
+      });
+
+      scene.components["networked-scene"].connect().catch(connectError => {
+        // hacky until we get return codes
+        const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i);
+        remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" });
+        exitScene();
+
+        return;
+      });
+
+      if (mediaStream) {
+        NAF.connection.adapter.setLocalMediaStream(mediaStream);
+
+        if (screenEntity) {
+          screenEntity.setAttribute("visible", sharingScreen);
+        } else if (sharingScreen) {
+          const sceneEl = document.querySelector("a-scene");
+          screenEntity = document.createElement("a-entity");
+          screenEntity.id = screenEntityId;
+          screenEntity.setAttribute("offset-relative-to", {
+            target: "#player-camera",
+            offset: "0 0 -2",
+            on: "action_share_screen"
+          });
+          screenEntity.setAttribute("networked", { template: "#video-template" });
+          sceneEl.appendChild(screenEntity);
+        }
+      }
+    }
   };
 
+  remountUI({ enterScene, exitScene });
+
   getAvailableVREntryTypes().then(availableVREntryTypes => {
     remountUI({ availableVREntryTypes });
   });
@@ -302,8 +310,10 @@ const onReady = async () => {
   console.log(`Hub ID: ${hubId}`);
 
   const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:";
-  const socketPort = qs.phx_port || document.location.port;
-  const socketHost = qs.phx_host || document.location.hostname;
+  const socketPort = qs.phx_port || (process.env.NODE_ENV === "production" ? document.location.port : 443);
+  const socketHost =
+    qs.phx_host ||
+    (process.env.NODE_ENV === "production" ? document.location.hostname : process.env.DEV_RETICULUM_SERVER);
   const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`;
   console.log(`Phoenix Channel URL: ${socketUrl}`);
 
@@ -322,7 +332,14 @@ const onReady = async () => {
       initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`);
       hubChannel.setPhoenixChannel(channel);
     })
-    .receive("error", res => console.error(res));
+    .receive("error", res => {
+      if (res.reason === "closed") {
+        exitScene();
+        remountUI({ roomUnavailableReason: "closed" });
+      }
+
+      console.error(res);
+    });
 };
 
 document.addEventListener("DOMContentLoaded", onReady);
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index d91dfdbed6e726a785cbb8744d35e3dab066085c..88d697a2d15ffc1ebfb4528f0e38f6de7134656e 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -19,9 +19,10 @@ const messages = localeData[lang] || localeData.en;
 
 const ENVIRONMENT_URLS = [
   "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/atrium/AtriumMeshes.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/courtyard/CourtyardMeshes.bundle.json"
+  "https://asset-bundles-prod.reticulum.io/rooms/theater/Theater.bundle.json",
+  "https://asset-bundles-prod.reticulum.io/rooms/atrium/Atrium.bundle.json",
+  "https://asset-bundles-prod.reticulum.io/rooms/courtyard/Courtyard.bundle.json",
+  "https://asset-bundles-prod.reticulum.io/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json"
 ];
 
 class HomeRoot extends Component {
@@ -38,7 +39,7 @@ class HomeRoot extends Component {
 
   componentDidMount() {
     this.loadEnvironments();
-    document.querySelector("#background-video").playbackRate = 0.5;
+    document.querySelector("#background-video").playbackRate = 0.75;
   }
 
   showDialog = dialogType => {
@@ -265,6 +266,16 @@ class HomeRoot extends Component {
                 <div className="footer-content__links__bottom">
                   <FormattedMessage id="home.made_with_love" />
                   <span style={{ fontWeight: "bold", color: "white" }}>moz://a</span>
+                  <span>
+                    &nbsp;&nbsp;|&nbsp;&nbsp;Medieval Fantasy Book by{" "}
+                    <a
+                      target="_blank"
+                      rel="noreferrer noopener"
+                      href="https://sketchfab.com/models/06d5a80a04fc4c5ab552759e9a97d91a?utm_campaign=06d5a80a04fc4c5ab552759e9a97d91a&utm_medium=embed&utm_source=oembed"
+                    >
+                      Pixel
+                    </a>
+                  </span>
                 </div>
               </div>
             </div>
diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js
index 32fd19b4edfcb343b0cada0d4f0abf0052344d8e..6e689653bcc4129547ec780cacbe220cb9f954da 100644
--- a/src/react-components/hub-create-panel.js
+++ b/src/react-components/hub-create-panel.js
@@ -38,14 +38,25 @@ class HubCreatePanel extends Component {
       hub: { name: this.state.name, default_environment_gltf_bundle_url: environment.bundle_url }
     };
 
-    const res = await fetch("/api/v1/hubs", {
+    let createUrl = "/api/v1/hubs";
+
+    if (process.env.NODE_ENV === "development") {
+      createUrl = `https://${process.env.DEV_RETICULUM_SERVER}${createUrl}`;
+    }
+
+    const res = await fetch(createUrl, {
       body: JSON.stringify(payload),
       headers: { "content-type": "application/json" },
       method: "POST"
     });
 
     const hub = await res.json();
-    document.location = hub.url;
+
+    if (process.env.NODE_ENV === "production") {
+      document.location = hub.url;
+    } else {
+      document.location = `/hub.html?hub_id=${hub.hub_id}`;
+    }
   };
 
   isHubNameValid = () => {
@@ -149,12 +160,22 @@ class HubCreatePanel extends Component {
                         {environmentTitle}
                       </span>
                       {environmentAuthor &&
-                        environmentAuthor.name && (
+                        environmentAuthor.name &&
+                        (environmentAuthor.url ? (
+                          <a
+                            href={environmentAuthor.url}
+                            target="_blank"
+                            className="create-panel__form__environment__picker__labels__header__author"
+                          >
+                            <FormattedMessage id="home.environment_author_by" />
+                            <span>{environmentAuthor.name}</span>
+                          </a>
+                        ) : (
                           <span className="create-panel__form__environment__picker__labels__header__author">
                             <FormattedMessage id="home.environment_author_by" />
                             <span>{environmentAuthor.name}</span>
                           </span>
-                        )}
+                        ))}
                     </div>
                     <div className="create-panel__form__environment__picker__labels__footer">
                       <FormattedMessage id="home.environment_picker_footer" />
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 2883d23f066498e941d561b6722b9fc5b434590c..512cb62c14750d01c8cd9cbbff7c3bc2b8b9cc6d 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -64,7 +64,8 @@ class UIRoot extends Component {
     showProfileEntry: PropTypes.bool,
     availableVREntryTypes: PropTypes.object,
     initialEnvironmentLoaded: PropTypes.bool,
-    janusRoomId: PropTypes.number
+    janusRoomId: PropTypes.number,
+    roomUnavailableReason: PropTypes.string
   };
 
   state = {
@@ -505,33 +506,54 @@ class UIRoot extends Component {
   };
 
   render() {
-    if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) {
+    if (this.state.exited || this.props.roomUnavailableReason) {
+      let subtitle = null;
+      if (this.props.roomUnavailableReason !== "closed") {
+        const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : this.props.roomUnavailableReason}`;
+        subtitle = <FormattedMessage id={exitSubtitleId} />;
+      } else {
+        // TODO i18n, due to links and markup
+        subtitle = (
+          <div>
+            Sorry, this room is no longer available.
+            <p />
+            A room may be closed if we receive reports that it violates our{" "}
+            <a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md">
+              Terms of Use
+            </a>.
+            <br />
+            If you have questions, contact us at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>.
+            <p />
+            If you&apos;d like to run your own server, Hubs&apos;s source code is available on{" "}
+            <a href="https://github.com/mozilla/hubs">Github</a>.
+          </div>
+        );
+      }
+
       return (
         <IntlProvider locale={lang} messages={messages}>
-          <div className="loading-panel">
-            <div className="loader-wrap">
-              <div className="loader">
-                <div className="loader-center" />
-              </div>
-            </div>
-            <div className="loading-panel__title">
+          <div className="exited-panel">
+            <div className="exited-panel__title">
               <b>moz://a</b> duck
             </div>
+            <div className="exited-panel__subtitle">{subtitle}</div>
           </div>
         </IntlProvider>
       );
     }
 
-    if (this.state.exited) {
+    if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) {
       return (
         <IntlProvider locale={lang} messages={messages}>
-          <div className="exited-panel">
+          <div className="loading-panel">
+            <div className="loader-wrap">
+              <div className="loader">
+                <div className="loader-center" />
+              </div>
+            </div>
             <div className="loading-panel__title">
               <b>moz://a</b> duck
             </div>
-            <div className="loading-panel__subtitle">
-              <FormattedMessage id="exit.subtitle" />
-            </div>
           </div>
         </IntlProvider>
       );
diff --git a/src/telemetry.js b/src/telemetry.js
index 6eed4d002b96c507dcdaea78b6a5ab58f9791441..9fa4ae9ce79009d203fb4992cd6d643460633794 100644
--- a/src/telemetry.js
+++ b/src/telemetry.js
@@ -2,6 +2,6 @@ import Raven from "raven-js";
 
 export default function registerTelemetry() {
   if (process.env.NODE_ENV === "production") {
-    Raven.config("https://f571beaf5cee4e3085e0bf436f3eb158@sentry.io/256771").install();
+    Raven.config("https://013d6a364fed43cdb0539a61d520597a@sentry.prod.mozaws.net/370").install();
   }
 }
diff --git a/webpack.config.js b/webpack.config.js
index 4f9b26d26cab4c3959a5b1796696f7a0b531c086..55bae82132e8d8611ba8b01ffcddf1ac49109c4b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -87,7 +87,7 @@ const config = {
   mode: "development",
   devtool: process.env.NODE_ENV === "production" ? "source-map" : "inline-source-map",
   devServer: {
-    open: true,
+    open: false,
     https: createHTTPSConfig(),
     host: "0.0.0.0",
     useLocalIp: true,
@@ -216,7 +216,8 @@ const config = {
     new webpack.DefinePlugin({
       "process.env": JSON.stringify({
         NODE_ENV: process.env.NODE_ENV,
-        JANUS_SERVER: process.env.JANUS_SERVER
+        JANUS_SERVER: process.env.JANUS_SERVER,
+        DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER
       })
     })
   ]