diff --git a/src/assets/images/hub-preview-white.png b/src/assets/images/hub-preview-white.png
new file mode 100755
index 0000000000000000000000000000000000000000..dd83924b037ec54eafc7ca63a14557ed4b44c473
Binary files /dev/null and b/src/assets/images/hub-preview-white.png differ
diff --git a/src/assets/stylesheets/scene-ui.scss b/src/assets/stylesheets/scene-ui.scss
new file mode 100644
index 0000000000000000000000000000000000000000..be2c9626bb5ae2fdd99e3919e04ba9bd01cdfb8a
--- /dev/null
+++ b/src/assets/stylesheets/scene-ui.scss
@@ -0,0 +1,106 @@
+@import 'shared';
+
+:local(.ui) {
+  @extend %default-font;
+
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  position: absolute;
+  pointer-events: none;
+  color: white;
+}
+
+:local(.whiteOverlay) {
+  background-color: rgba(255, 255, 255, 0.3);
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  position: absolute;
+}
+
+:local(.grid) {
+  display: grid;
+  grid-template-columns: 1fr 20px minmax(200px, 400px) 20px 1fr;
+  grid-template-rows: 1fr 3fr 1fr;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(255, 255, 255, 0.2);
+}
+
+:local(.mainPanel) {
+  grid-column: 3;
+  grid-row: 2;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  pointer-events: auto;
+
+  button {
+    @extend %action-button;
+    border: 0;
+  }
+}
+
+:local(.logoTagline) {
+  color: black;
+  text-shadow: 0px 0px 20px #aaa;
+  font-weight: bold;
+  text-align: center;
+  font-size: 1.2em;
+  margin-bottom: 32px;
+}
+
+:local(.logo) {
+  width: 100%;
+  display: block;
+
+  img {
+    width: 100%;
+  }
+}
+
+:local(.info) {
+  position: absolute;
+  color: black;
+  text-shadow: 0px 0px 20px #aaa;
+  bottom: 12px;
+  left: 12px;
+  display: flex;
+  flex-direction: column;
+}
+
+:local(.name) {
+  font-weight: bold;
+  font-size: 1.6em;
+}
+
+:local(.attribution) {
+  font-size: 1.0em;
+  white-space: wrap;
+}
+
+:local(.screenshot) {
+  position: absolute;
+  width: 115%;
+  height: 115%;
+  top: -40px;
+  left: -40px;
+
+  img {
+    width: 100%;
+    height: 100%;
+  }
+
+  background-color: black;
+  filter: blur(10px);
+}
+
+:local(.screenshotHidden) {
+  visibility: hidden;
+  opacity: 0;
+  transition: visibility 0s 0.5s, opacity 0.5s linear;
+}
diff --git a/src/assets/stylesheets/scene.scss b/src/assets/stylesheets/scene.scss
new file mode 100644
index 0000000000000000000000000000000000000000..44e6591aacfa994bb79a4a0354b129ae171c4741
--- /dev/null
+++ b/src/assets/stylesheets/scene.scss
@@ -0,0 +1,2 @@
+@import 'shared';
+@import 'loader';
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index e3fe060ac208cb8ea4159235758664033228f299..d29f96029b44573b4c01929289087abd1f92bcb7 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -78,6 +78,8 @@
     "link.dont_have_a_code": "Don't have a code?",
     "link.create_a_room": "Create a Room",
     "link.try_again": "We couldn't find that code. Please try again.",
-    "help.report_issue": "Report an Issue"
+    "help.report_issue": "Report an Issue",
+    "scene.logo_tagline": "A new way to get together",
+    "scene.create_button": "create a room with this scene"
   }
 }
diff --git a/src/components/scene-components.js b/src/components/scene-components.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7e562787e60612b1c5b1a57147416746898f1a2
--- /dev/null
+++ b/src/components/scene-components.js
@@ -0,0 +1,23 @@
+import "./ambient-light";
+import "./animation-mixer";
+import "./audio-feedback";
+import "./css-class";
+import "./directional-light";
+import "./duck";
+import "./gltf-model-plus";
+import "./heightfield";
+import "./hemisphere-light";
+import "./hide-when-quality";
+import "./layers";
+import "./loop-animation";
+import "./media-loader";
+import "./point-light";
+import "./quack";
+import "./scene-shadow";
+import "./scene-preview-camera";
+import "./skybox";
+import "./spawn-controller";
+import "./spot-light";
+import "./sticky-object";
+import "./super-spawner";
+import "./water";
diff --git a/src/components/scene-preview-camera.js b/src/components/scene-preview-camera.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b7beaa29679518f63fceda44052f9efe37c2a1e
--- /dev/null
+++ b/src/components/scene-preview-camera.js
@@ -0,0 +1,50 @@
+/**
+ * Nicely pans the camera for previewing a scene. There's some weirdness with this right now
+ * since it ends up panning in a direction dependent upon the start camera orientation,
+ * but it's good enough for now.
+ */
+function lerp(start, end, t) {
+  return (1 - t) * start + t * end;
+}
+
+const DURATION = 90.0;
+
+AFRAME.registerComponent("scene-preview-camera", {
+  init: function() {
+    this.startPoint = this.el.object3D.position.clone();
+    this.startRotation = new THREE.Quaternion();
+    this.startRotation.setFromEuler(this.el.object3D.rotation);
+
+    this.targetPoint = new THREE.Vector3(1, 0.5, -0.5);
+    this.targetPoint.applyMatrix4(this.el.object3D.matrix);
+    this.targetPoint.add(new THREE.Vector3(0, 0, -2));
+
+    const targetRotDelta = new THREE.Euler(-0.15, 0.0, 0.15);
+    this.targetRotation = new THREE.Quaternion();
+    this.targetRotation.setFromEuler(targetRotDelta);
+    this.targetRotation.premultiply(this.startRotation);
+
+    this.startTime = new Date().getTime();
+    this.backwards = false;
+  },
+
+  tick: function() {
+    const t = (new Date().getTime() - this.startTime) / (1000.0 * DURATION);
+
+    const from = this.backwards ? this.targetPoint : this.startPoint;
+    const to = this.backwards ? this.startPoint : this.targetPoint;
+    const fromRot = this.backwards ? this.targetRotation : this.startRotation;
+    const toRot = this.backwards ? this.startRotation : this.targetRotation;
+    const newRot = new THREE.Quaternion();
+
+    THREE.Quaternion.slerp(fromRot, toRot, newRot, t);
+
+    this.el.object3D.position.set(lerp(from.x, to.x, t), lerp(from.y, to.y, t), lerp(from.z, to.z, t));
+    this.el.object3D.rotation.setFromQuaternion(newRot);
+
+    if (t >= 0.99) {
+      this.backwards = !this.backwards;
+      this.startTime = new Date().getTime();
+    }
+  }
+});
diff --git a/src/hub.html b/src/hub.html
index deb09091418edc0b49e8e11758bfa4f94d2bf203..4d297a996c477ad06f124674a1c3554959779b50 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -2,7 +2,7 @@
 <html>
 
 <head>
-    <!-- DO NOT REMOVE/EDIT THIS COMMENT - HUB_META_TAGS -->
+    <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS -->
 
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
diff --git a/src/hub.js b/src/hub.js
index ce0e5dca5e8486bdc172e72562fda1367d3f36ac..590b9d5dae21e89b6d90fbed14209fe4a2a637c3 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -31,9 +31,9 @@ import { ObjectContentOrigins } from "./object-types";
 
 import "./activators/shortpress";
 
+import "./components/scene-components";
 import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future
 import "./components/mute-mic";
-import "./components/audio-feedback";
 import "./components/bone-mute-state-indicator";
 import "./components/bone-visibility";
 import "./components/in-world-hud";
@@ -44,17 +44,9 @@ import "./components/character-controller";
 import "./components/haptic-feedback";
 import "./components/networked-video-player";
 import "./components/offset-relative-to";
-import "./components/water";
-import "./components/skybox";
-import "./components/layers";
-import "./components/spawn-controller";
-import "./components/hide-when-quality";
 import "./components/player-info";
 import "./components/debug";
-import "./components/animation-mixer";
-import "./components/loop-animation";
 import "./components/hand-poses";
-import "./components/gltf-model-plus";
 import "./components/gltf-bundle";
 import "./components/hud-controller";
 import "./components/freeze-controller";
@@ -64,26 +56,17 @@ import "./components/block-button";
 import "./components/visible-while-frozen";
 import "./components/stats-plus";
 import "./components/networked-avatar";
-import "./components/css-class";
-import "./components/scene-shadow";
 import "./components/avatar-replay";
 import "./components/media-views";
 import "./components/pinch-to-move";
 import "./components/look-on-mobile";
 import "./components/pitch-yaw-rotator";
 import "./components/input-configurator";
-import "./components/sticky-object";
 import "./components/auto-scale-cannon-physics-body";
 import "./components/position-at-box-shape-border";
 import "./components/remove-networked-object-button";
 import "./components/destroy-at-extreme-distances";
-import "./components/media-loader";
 import "./components/gamma-factor";
-import "./components/ambient-light";
-import "./components/directional-light";
-import "./components/hemisphere-light";
-import "./components/point-light";
-import "./components/spot-light";
 import "./components/visible-to-owner";
 import "./components/camera-tool";
 
@@ -123,18 +106,13 @@ import "aframe-physics-extras";
 import "super-hands";
 import "./components/super-networked-interactable";
 import "./components/networked-counter";
-import "./components/super-spawner";
 import "./components/event-repeater";
 import "./components/controls-shape-offset";
-import "./components/duck";
-import "./components/quack";
-import "./components/grabbable-toggle";
 
 import "./components/cardboard-controls";
 
 import "./components/cursor-controller";
 
-import "./components/heightfield";
 import "./components/nav-mesh-helper";
 import "./systems/tunnel-effect";
 
diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js
new file mode 100644
index 0000000000000000000000000000000000000000..ec04e095e135bac8bb85beff8c27c7cf2b6b3ae9
--- /dev/null
+++ b/src/react-components/scene-ui.js
@@ -0,0 +1,104 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
+import en from "react-intl/locale-data/en";
+import styles from "../assets/stylesheets/scene-ui.scss";
+import hubLogo from "../assets/images/hub-preview-white.png";
+import { getReticulumFetchUrl } from "../utils/phoenix-utils";
+import { generateHubName } from "../utils/name-generation";
+
+import { lang, messages } from "../utils/i18n";
+
+addLocaleData([...en]);
+
+class SceneUI extends Component {
+  static propTypes = {
+    scene: PropTypes.object,
+    sceneLoaded: PropTypes.bool,
+    sceneId: PropTypes.string,
+    sceneName: PropTypes.string,
+    sceneDescription: PropTypes.string,
+    sceneAttribution: PropTypes.string,
+    sceneScreenshotURL: PropTypes.string
+  };
+
+  state = {
+    showScreenshot: false
+  };
+
+  constructor(props) {
+    super(props);
+
+    // Show screenshot if scene isn't loaded in 5 seconds
+    setTimeout(() => {
+      if (!this.props.sceneLoaded) {
+        this.setState({ showScreenshot: true });
+      }
+    }, 5000);
+  }
+
+  componentDidMount() {
+    this.props.scene.addEventListener("loaded", this.onSceneLoaded);
+  }
+
+  componentWillUnmount() {
+    this.props.scene.removeEventListener("loaded", this.onSceneLoaded);
+  }
+
+  createRoom = async () => {
+    const payload = { hub: { name: generateHubName(), scene_id: this.props.sceneId } };
+    const createUrl = getReticulumFetchUrl("/api/v1/hubs");
+
+    const res = await fetch(createUrl, {
+      body: JSON.stringify(payload),
+      headers: { "content-type": "application/json" },
+      method: "POST"
+    });
+
+    const hub = await res.json();
+
+    if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) {
+      document.location = hub.url;
+    } else {
+      document.location = `/hub.html?hub_id=${hub.hub_id}`;
+    }
+  };
+
+  render() {
+    return (
+      <IntlProvider locale={lang} messages={messages}>
+        <div className={styles.ui}>
+          <div
+            className={classNames({
+              [styles.screenshot]: true,
+              [styles.screenshotHidden]: this.props.sceneLoaded
+            })}
+          >
+            {this.state.showScreenshot && <img src={this.props.sceneScreenshotURL} />}
+          </div>
+          <div className={styles.whiteOverlay} />
+          <div className={styles.grid}>
+            <div className={styles.mainPanel}>
+              <a href="/" className={styles.logo}>
+                <img src={hubLogo} />
+              </a>
+              <div className={styles.logoTagline}>
+                <FormattedMessage id="scene.logo_tagline" />
+              </div>
+              <button onClick={this.createRoom}>
+                <FormattedMessage id="scene.create_button" />
+              </button>
+            </div>
+          </div>
+          <div className={styles.info}>
+            <div className={styles.name}>{this.props.sceneName}</div>
+            <div className={styles.attribution}>{this.props.sceneAttribution}</div>
+          </div>
+        </div>
+      </IntlProvider>
+    );
+  }
+}
+
+export default SceneUI;
diff --git a/src/scene.html b/src/scene.html
new file mode 100644
index 0000000000000000000000000000000000000000..222fa5d3be5878784e0612cc92c3717624a40036
--- /dev/null
+++ b/src/scene.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <!-- DO NOT REMOVE/EDIT THIS COMMENT - META_TAGS -->
+
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+
+    <link rel="shortcut icon" type="image/png" href="/favicon.ico">
+    <title>Scene | Hubs by Mozilla</title>
+    <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet">
+</head>
+
+<body>
+    <a-scene
+        renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;"
+        vr-mode-ui="enabled: false"
+        gamma-factor
+    >
+        <a-entity
+            id="scene-root"
+            static-body="shape: none;"
+        ></a-entity>
+
+        <a-camera id="camera" fov="80" look-controls="enabled: false" wasd-controls="enabled: false"></a-camera>
+    </a-scene>
+
+    <div id="ui-root"></div>
+</body>
+
+</html>
diff --git a/src/scene.js b/src/scene.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f2f0732d0da231b0bebef56b8ce526326aa5ea4
--- /dev/null
+++ b/src/scene.js
@@ -0,0 +1,110 @@
+console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`);
+
+import "./assets/stylesheets/scene.scss";
+
+import "aframe";
+import "./utils/logging";
+import { patchWebGLRenderingContext } from "./utils/webgl";
+patchWebGLRenderingContext();
+
+import "three/examples/js/loaders/GLTFLoader";
+
+import "./components/scene-components";
+import "./components/debug";
+import "./systems/nav";
+
+import { getReticulumFetchUrl } from "./utils/phoenix-utils";
+
+import ReactDOM from "react-dom";
+import React from "react";
+import SceneUI from "./react-components/scene-ui";
+import { disableiOSZoom } from "./utils/disable-ios-zoom";
+
+import "./gltf-component-mappings";
+
+import { App } from "./App";
+
+window.APP = new App();
+
+const qs = new URLSearchParams(location.search);
+const isMobile = AFRAME.utils.device.isMobile();
+
+window.APP.quality = qs.get("quality") || isMobile ? "low" : "high";
+
+import "aframe-physics-system";
+import "aframe-physics-extras";
+import "./components/event-repeater";
+import "./components/controls-shape-offset";
+
+import registerTelemetry from "./telemetry";
+
+registerTelemetry();
+
+disableiOSZoom();
+
+function mountUI(scene, props = {}) {
+  ReactDOM.render(
+    <SceneUI
+      {...{
+        scene,
+        ...props
+      }}
+    />,
+    document.getElementById("ui-root")
+  );
+}
+
+const onReady = async () => {
+  const scene = document.querySelector("a-scene");
+  window.APP.scene = scene;
+
+  const sceneId = qs.get("scene_id") || document.location.pathname.substring(1).split("/")[1];
+  console.log(`Scene ID: ${sceneId}`);
+
+  let uiProps = { sceneId: sceneId };
+
+  mountUI(scene);
+
+  const remountUI = props => {
+    uiProps = { ...uiProps, ...props };
+    mountUI(scene, uiProps);
+  };
+
+  const sceneRoot = document.querySelector("#scene-root");
+  const sceneModelEntity = document.createElement("a-entity");
+  const gltfEl = document.createElement("a-entity");
+  const camera = document.getElementById("camera");
+
+  sceneModelEntity.addEventListener("scene-loaded", () => {
+    remountUI({ sceneLoaded: true });
+    const previewCamera = gltfEl.object3D.getObjectByName("scene-preview-camera");
+
+    if (previewCamera) {
+      camera.object3D.position.copy(previewCamera.position);
+      camera.object3D.rotation.copy(previewCamera.rotation);
+      camera.object3D.updateMatrix();
+    }
+
+    camera.setAttribute("scene-preview-camera", "");
+  });
+
+  const res = await fetch(getReticulumFetchUrl(`/api/v1/scenes/${sceneId}`)).then(r => r.json());
+  const sceneInfo = res.scenes[0];
+
+  const modelUrl = sceneInfo.model_url;
+  console.log(`Scene Model URL: ${modelUrl}`);
+
+  gltfEl.setAttribute("gltf-model-plus", { src: modelUrl, useCache: false, inflate: true });
+  gltfEl.addEventListener("model-loaded", () => sceneModelEntity.emit("scene-loaded"));
+  sceneModelEntity.appendChild(gltfEl);
+  sceneRoot.appendChild(sceneModelEntity);
+
+  remountUI({
+    sceneName: sceneInfo.name,
+    sceneDescription: sceneInfo.description,
+    sceneAttribution: sceneInfo.attribution,
+    sceneScreenshotURL: sceneInfo.screenshot_url
+  });
+};
+
+document.addEventListener("DOMContentLoaded", onReady);
diff --git a/webpack.config.js b/webpack.config.js
index 9531ceb26e70881f4bd4b5af7eb4367e2141f007..4c28ef4a04acaba07f91c52bf7d4bf804de6501d 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -64,6 +64,7 @@ module.exports = (env, argv) => ({
   entry: {
     index: path.join(__dirname, "src", "index.js"),
     hub: path.join(__dirname, "src", "hub.js"),
+    scene: path.join(__dirname, "src", "scene.js"),
     link: path.join(__dirname, "src", "link.js"),
     "avatar-selector": path.join(__dirname, "src", "avatar-selector.js")
   },
@@ -196,6 +197,20 @@ module.exports = (env, argv) => ({
         }
       ]
     }),
+    new HTMLWebpackPlugin({
+      filename: "scene.html",
+      template: path.join(__dirname, "src", "scene.html"),
+      chunks: ["vendor", "engine", "scene"],
+      inject: "head",
+      meta: [
+        {
+          "http-equiv": "origin-trial",
+          "data-feature": "WebVR (For Chrome M62+)",
+          "data-expires": process.env.ORIGIN_TRIAL_EXPIRES,
+          content: process.env.ORIGIN_TRIAL_TOKEN
+        }
+      ]
+    }),
     new HTMLWebpackPlugin({
       filename: "link.html",
       template: path.join(__dirname, "src", "link.html"),