diff --git a/public/app.js b/public/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..04294f74c16ccbca3b249d1522d38f68f964c3bd
--- /dev/null
+++ b/public/app.js
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Query for WebXR support. If there's no support for the `immersive-ar` mode,
+ * show an error.
+ */
+(async function() {
+  const isArSessionSupported = navigator.xr && navigator.xr.isSessionSupported && await navigator.xr.isSessionSupported("immersive-ar");
+  if (isArSessionSupported) {
+    document.getElementById("enter-ar").addEventListener("click", window.app.activateXR)
+  } else {
+    onNoXRDevice();
+  }
+})();
+
+/**
+ * Container class to manage connecting to the WebXR Device API
+ * and handle rendering on every frame.
+ */
+class App {
+  /**
+   * Run when the Start AR button is pressed.
+   */
+  activateXR = async () => {
+    try {
+      // Initialize a WebXR session using "immersive-ar".
+      this.xrSession = await navigator.xr.requestSession("immersive-ar", {
+        requiredFeatures: ['hit-test', 'dom-overlay'],
+        domOverlay: { root: document.body }
+      });
+
+      // Create the canvas that will contain our camera's background and our virtual scene.
+      this.createXRCanvas();
+
+      // With everything set up, start the app.
+      await this.onSessionStarted();
+    } catch(e) {
+      console.log(e);
+      onNoXRDevice();
+    }
+  }
+
+  /**
+   * Add a canvas element and initialize a WebGL context that is compatible with WebXR.
+   */
+  createXRCanvas() {
+    this.canvas = document.createElement("canvas");
+    document.body.appendChild(this.canvas);
+    this.gl = this.canvas.getContext("webgl", {xrCompatible: true});
+
+    this.xrSession.updateRenderState({
+      baseLayer: new XRWebGLLayer(this.xrSession, this.gl)
+    });
+  }
+
+  /**
+   * Called when the XRSession has begun. Here we set up our three.js
+   * renderer, scene, and camera and attach our XRWebGLLayer to the
+   * XRSession and kick off the render loop.
+   */
+  onSessionStarted = async () => {
+    // Add the `ar` class to our body, which will hide our 2D components
+    document.body.classList.add('ar');
+
+    // To help with working with 3D on the web, we'll use three.js.
+    this.setupThreeJs();
+
+    // Setup an XRReferenceSpace using the "local" coordinate system.
+    this.localReferenceSpace = await this.xrSession.requestReferenceSpace('local');
+
+    // Create another XRReferenceSpace that has the viewer as the origin.
+    this.viewerSpace = await this.xrSession.requestReferenceSpace('viewer');
+    // Perform hit testing using the viewer as origin.
+    this.hitTestSource = await this.xrSession.requestHitTestSource({ space: this.viewerSpace });
+
+    // Start a rendering loop using this.onXRFrame.
+    this.xrSession.requestAnimationFrame(this.onXRFrame);
+
+    this.xrSession.addEventListener("select", this.onSelect);
+  }
+
+  /** Place a sunflower when the screen is tapped. */
+  onSelect = () => {
+    if (window.sunflower) {
+      const clone = window.sunflower.clone();
+      clone.position.copy(this.reticle.position);
+      this.scene.add(clone)
+
+      const shadowMesh = this.scene.children.find(c => c.name === 'shadowMesh');
+      shadowMesh.position.y = clone.position.y;
+    }
+  }
+
+  /**
+   * Called on the XRSession's requestAnimationFrame.
+   * Called with the time and XRPresentationFrame.
+   */
+  onXRFrame = (time, frame) => {
+    // Queue up the next draw request.
+    this.xrSession.requestAnimationFrame(this.onXRFrame);
+
+    // Bind the graphics framebuffer to the baseLayer's framebuffer.
+    const framebuffer = this.xrSession.renderState.baseLayer.framebuffer
+    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer)
+    this.renderer.setFramebuffer(framebuffer);
+
+    // Retrieve the pose of the device.
+    // XRFrame.getViewerPose can return null while the session attempts to establish tracking.
+    const pose = frame.getViewerPose(this.localReferenceSpace);
+    if (pose) {
+      // In mobile AR, we only have one view.
+      const view = pose.views[0];
+
+      const viewport = this.xrSession.renderState.baseLayer.getViewport(view);
+      this.renderer.setSize(viewport.width, viewport.height)
+
+      // Use the view's transform matrix and projection matrix to configure the THREE.camera.
+      this.camera.matrix.fromArray(view.transform.matrix)
+      this.camera.projectionMatrix.fromArray(view.projectionMatrix);
+      this.camera.updateMatrixWorld(true);
+
+      // Conduct hit test.
+      const hitTestResults = frame.getHitTestResults(this.hitTestSource);
+
+      // If we have results, consider the environment stabilized.
+      if (!this.stabilized && hitTestResults.length > 0) {
+        this.stabilized = true;
+        document.body.classList.add('stabilized');
+      }
+      if (hitTestResults.length > 0) {
+        const hitPose = hitTestResults[0].getPose(this.localReferenceSpace);
+
+        // Update the reticle position
+        this.reticle.visible = true;
+        this.reticle.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z)
+        this.reticle.updateMatrixWorld(true);
+      }
+
+      // Render the scene with THREE.WebGLRenderer.
+      this.renderer.render(this.scene, this.camera)
+    }
+  }
+
+  /**
+   * Initialize three.js specific rendering code, including a WebGLRenderer,
+   * a demo scene, and a camera for viewing the 3D content.
+   */
+  setupThreeJs() {
+    // To help with working with 3D on the web, we'll use three.js.
+    // Set up the WebGLRenderer, which handles rendering to our session's base layer.
+    this.renderer = new THREE.WebGLRenderer({
+      alpha: true,
+      preserveDrawingBuffer: true,
+      canvas: this.canvas,
+      context: this.gl
+    });
+    this.renderer.autoClear = false;
+    this.renderer.shadowMap.enabled = true;
+    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+    // Initialize our demo scene.
+    this.scene = DemoUtils.createLitScene();
+    this.reticle = new Reticle();
+    this.scene.add(this.reticle);
+
+    // We'll update the camera matrices directly from API, so
+    // disable matrix auto updates so three.js doesn't attempt
+    // to handle the matrices independently.
+    this.camera = new THREE.PerspectiveCamera();
+    this.camera.matrixAutoUpdate = false;
+  }
+};
+
+window.app = new App();