diff --git a/Jenkinsfile b/Jenkinsfile
index 18a2532f8236797c5f074eadcd9b6c160a9ef22a..bf031771306c10109afbc40a165d66b40b27e698 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,3 +1,19 @@
+import groovy.json.JsonOutput
+
+// From https://issues.jenkins-ci.org/browse/JENKINS-44231
+
+// Given arbitrary string returns a strongly escaped shell string literal.
+// I.e. it will be in single quotes which turns off interpolation of $(...), etc.
+// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command).
+def shellString(s) {
+  // Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'.
+  // 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way.
+  // 2) And why not use ANSI-C quoting? I.e. we could replace ' with \'
+  // and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573).
+  // Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873).
+  '\'' + s.replace('\'', '\'\\\'\'') + '\''
+}
+
 pipeline {
   agent any
 
@@ -6,9 +22,41 @@ pipeline {
   }
 
   stages {
+    stage('pre-build') {
+      steps {
+        sh 'rm -rf ./build ./tmp'
+      }
+    }
+
     stage('build') {
       steps {
-        build 'reticulum'
+        script {
+          def baseAssetsPath = env.BASE_ASSETS_PATH
+          def assetBundleServer = env.ASSET_BUNDLE_SERVER
+          def targetS3Url = env.TARGET_S3_URL
+          def smokeURL = env.SMOKE_URL
+          def slackURL = env.SLACK_URL
+
+          def habCommand = "sudo /usr/bin/hab-docker-studio -k mozillareality run /bin/bash scripts/hab-build-and-push.sh ${baseAssetsPath} ${assetBundleServer} ${targetS3Url} ${env.BUILD_NUMBER}"
+          sh "/usr/bin/script --return -c ${shellString(habCommand)} /dev/null"
+
+          def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim()
+          def gitSha = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
+          def text = (
+            "*<http://localhost:8080/job/${env.JOB_NAME}/${env.BUILD_NUMBER}|#${env.BUILD_NUMBER}>* *${env.JOB_NAME}* " +
+            "<https://github.com/mozilla/hubs/commit/$gitSha|$gitSha> " +
+            "Hubs: ```${gitSha} ${gitMessage}```\n" +
+            "<${smokeURL}?require_version=${env.BUILD_NUMBER}|Smoke Test> - to push:\n" +
+            "`/mr hubs deploy ${targetS3Url}`"
+          )
+          def payload = 'payload=' + JsonOutput.toJson([
+            text      : text,
+            channel   : "#mr-builds",
+            username  : "buildbot",
+            icon_emoji: ":gift:"
+          ])
+          sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}"
+        }
       }
     }
   }
diff --git a/scripts/build_local_reticulum.sh b/scripts/build_local_reticulum.sh
deleted file mode 100755
index 9a19f9f202b688b27213b0478f9e1a14e67d2620..0000000000000000000000000000000000000000
--- a/scripts/build_local_reticulum.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-if [ ! -e ../reticulum ]; then
-  echo "This script assumes reticulum is checked out in a sibling to this folder."
-fi
-
-rm -rf ../reticulum/priv/static ; GENERATE_SMOKE_TESTS=true BASE_ASSETS_PATH=https://hubs.local:4000/ yarn build -- --output-path ../reticulum/priv/static 
diff --git a/scripts/hab-build-and-push.sh b/scripts/hab-build-and-push.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2f04c0a26fd57505bfd508de05b56e920e947b37
--- /dev/null
+++ b/scripts/hab-build-and-push.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+export BASE_ASSETS_PATH=$1
+export ASSET_BUNDLE_SERVER=$2
+export TARGET_S3_URL=$3
+export BUILD_VERSION=$4
+
+# To build + push to S3 run:
+# hab studio run "bash scripts/hab-build-and-push.sh"
+
+# On exit, need to make all files writable so CI can clean on next build
+trap 'chmod -R a+rw .' EXIT
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+pushd "$DIR/.."
+
+mkdir -p .yarn
+mkdir -p node_modules
+mkdir -p build
+
+# Yarn expects /usr/local/share
+# https://github.com/yarnpkg/yarn/issues/4628
+mkdir -p /usr/local/share
+
+rm /usr/bin/env
+ln -s "$(hab pkg path core/coreutils)/bin/env" /usr/bin/env
+hab pkg install -b core/coreutils core/bash core/node core/yarn core/git core/aws-cli
+
+yarn install --cache-folder .yarn
+GENERATE_SMOKE_TESTS=true yarn build --output-path build
+mkdir build/pages
+mv build/*.html build/pages
+
+aws s3 sync --acl public-read --cache-control "max-age=31556926" build/assets "$TARGET_S3_URL/assets"
+aws s3 sync --acl public-read --cache-control "no-cache" --delete build/pages "$TARGET_S3_URL/pages/latest"
diff --git a/scripts/run-local-reticulum.sh b/scripts/run-local-reticulum.sh
new file mode 100644
index 0000000000000000000000000000000000000000..976d858cd8dd7adea49ae587995759c431420182
--- /dev/null
+++ b/scripts/run-local-reticulum.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+BASE_ASSETS_PATH=https://hubs.local:8080/ DEV_RETICULUM_SERVER=hubs.local:4000 yarn start
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 79d11599544849a83f9982617dafddd3bb6c268a..1f8ae6a1b0986b0576ad3dd567b1fc8bb96c1d86 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -40,6 +40,7 @@
     "exit.subtitle.closed": "This room is no longer available.",
     "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.",
+    "exit.subtitle.version_mismatch": "The version you deployed is not available yet. Your browser will refresh in 5 seconds.",
     "autoexit.title": "Auto-ending session in ",
     "autoexit.title_units": " seconds",
     "autoexit.subtitle": "You have started another session.",
diff --git a/src/hub.js b/src/hub.js
index fa21149111a673f2390b7acc6b33eaeb3dbfa840..d66de1f4ffae134ee535e1bc6b38a4baf562f9e7 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -432,6 +432,13 @@ const onReady = async () => {
     return;
   }
 
+  if (qs.required_version && qs.required_version !== process.env.BUILD_VERSION) {
+    remountUI({ roomUnavailableReason: "version_mismatch" });
+    setTimeout(() => document.location.reload(), 5000);
+    exitScene();
+    return;
+  }
+
   getAvailableVREntryTypes().then(availableVREntryTypes => {
     if (availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) {
       remountUI({ availableVREntryTypes, forcedVREntryType: "gearvr" });
diff --git a/src/index.js b/src/index.js
index 2383019035144bc82277e15baa01b034f6244754..32558cd4ec93844c558ce5d45fed8d54824ac01a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,7 +7,6 @@ import InfoDialog from "./react-components/info-dialog.js";
 import queryString from "query-string";
 
 const qs = queryString.parse(location.search);
-
 registerTelemetry();
 
 ReactDOM.render(
diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js
index 7e44ce63093ad5ad3df772807794140cab9af2b9..dc10b99ac217609b07bda59422c805c5ea4f1581 100644
--- a/src/react-components/hub-create-panel.js
+++ b/src/react-components/hub-create-panel.js
@@ -85,7 +85,7 @@ class HubCreatePanel extends Component {
 
     const hub = await res.json();
 
-    if (process.env.NODE_ENV === "production") {
+    if (process.env.NODE_ENV === "production" || document.location.host === process.env.DEV_RETICULUM_SERVER) {
       document.location = hub.url;
     } else {
       document.location = `/hub.html?hub_id=${hub.hub_id}`;
diff --git a/webpack.config.js b/webpack.config.js
index 3c92d1b38b746d1646696d0eb93d7b3f00fadd2b..5f688d9bfa687d8cc517eb0b98e9d70b74fd71d2 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -99,6 +99,7 @@ const config = {
     useLocalIp: true,
     public: "hubs.local:8080",
     port: 8080,
+    headers: { "Access-Control-Allow-Origin": "*" },
     before: function(app) {
       // networked-aframe makes HEAD requests to the server for time syncing. Respond with an empty body.
       app.head("*", function(req, res, next) {
@@ -266,7 +267,8 @@ const config = {
         NODE_ENV: process.env.NODE_ENV,
         JANUS_SERVER: process.env.JANUS_SERVER,
         DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER,
-        ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER
+        ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER,
+        BUILD_VERSION: process.env.BUILD_VERSION
       })
     })
   ]