diff --git a/package.json b/package.json
index 95b04a6073d46ef12bfce28af145e42bd741ad9d..3af88032ac14ddf9d383cf31dfc7b897c742d72e 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,16 @@
 {
-  "name": "mr-social-client",
+  "name": "hubs",
   "version": "0.0.1",
+  "description": "Duck-themed multi-user virtual spaces in WebVR.",
   "main": "src/index.js",
   "license": "MPL-2.0",
+  "homepage": "https://github.com/mozilla/hubs#readme",
   "repository": {
     "type": "git",
-    "url": "https://github.com/mozilla/mr-social-client.git"
+    "url": "https://github.com/mozilla/hubs.git"
+  },
+  "bugs": {
+    "url": "https://github.com/mozilla/hubs/issues"
   },
   "scripts": {
     "postinstall": "node ./scripts/postinstall.js",
@@ -33,7 +38,6 @@
     "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
     "classnames": "^2.2.5",
     "copy-to-clipboard": "^3.0.8",
-    "copy-webpack-plugin": "^4.5.1",
     "deepmerge": "^2.1.1",
     "detect-browser": "^2.1.0",
     "event-target-shim": "^3.0.1",
@@ -44,12 +48,10 @@
     "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master",
     "nipplejs": "https://github.com/mozillareality/nipplejs#mr-social-client/master",
     "phoenix": "^1.3.0",
-    "query-string": "^5.0.1",
     "raven-js": "^3.20.1",
     "react": "^16.1.1",
     "react-dom": "^16.1.1",
     "react-intl": "^2.4.0",
-    "react-router-dom": "^4.2.2",
     "screenfull": "^3.3.2",
     "super-hands": "https://github.com/mozillareality/aframe-super-hands-component#hubs/master",
     "uuid": "^3.2.1",
@@ -64,6 +66,7 @@
     "babel-plugin-transform-react-jsx-img-import": "^0.1.4",
     "babel-preset-env": "^1.6.1",
     "babel-preset-react": "^6.24.1",
+    "copy-webpack-plugin": "^4.5.1",
     "cross-env": "^5.1.3",
     "css-loader": "^0.28.10",
     "dotenv": "^5.0.1",
diff --git a/src/avatar-selector.js b/src/avatar-selector.js
index 45b0a7eed21aebe238e86237a540d97dbbdc63fe..296b94e4dd1608abd97e4a6f6e7f4f1270a80301 100644
--- a/src/avatar-selector.js
+++ b/src/avatar-selector.js
@@ -1,6 +1,5 @@
 import ReactDOM from "react-dom";
 import React from "react";
-import queryString from "query-string";
 import { IntlProvider, addLocaleData } from "react-intl";
 import en from "react-intl/locale-data/en";
 
@@ -26,22 +25,16 @@ addLocaleData([...en]);
 
 registerTelemetry();
 
+const hash = new URLSearchParams(location.hash.replace(/^#/, "?"));
 window.APP = new App();
-const hash = queryString.parse(location.hash);
-const isMobile = AFRAME.utils.device.isMobile();
-if (hash.quality) {
-  window.APP.quality = hash.quality;
-} else {
-  window.APP.quality = isMobile ? "low" : "high";
-}
+window.APP.quality = hash.get("quality") || AFRAME.utils.device.isMobile() ? "low" : "high";
 
 function postAvatarIdToParent(newAvatarId) {
   window.parent.postMessage({ avatarId: newAvatarId }, location.origin);
 }
 
 function mountUI() {
-  const hash = queryString.parse(location.hash);
-  const avatarId = hash.avatar_id;
+  const avatarId = hash.get("avatar_id");
   ReactDOM.render(
     <IntlProvider locale={lang} messages={messages}>
       <AvatarSelector {...{ avatars, avatarId, onChange: postAvatarIdToParent }} />
diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js
index eb4a93e0acc77173a714ca72aee1a29f744a2ffb..912c15bad2e2587960ad6ca484ffc13626009b24 100644
--- a/src/components/networked-video-player.js
+++ b/src/components/networked-video-player.js
@@ -1,5 +1,3 @@
-import queryString from "query-string";
-
 import styles from "./networked-video-player.css";
 
 const nafConnected = function() {
@@ -25,8 +23,8 @@ AFRAME.registerComponent("networked-video-player", {
 
     const ownerId = networkedEl.components.networked.data.owner;
 
-    const qs = queryString.parse(location.search);
-    const rejectScreenShares = qs.accept_screen_shares === undefined;
+    const qs = new URLSearchParams(location.search);
+    const rejectScreenShares = !qs.has("accept_screen_shares");
     if (ownerId !== NAF.clientId && rejectScreenShares) {
       // Toggle material visibility since object visibility is network-synced
       // TODO: There ought to be a better way to disable network syncs on a remote entity
diff --git a/src/hub.js b/src/hub.js
index 2d3863f11d8354d72b4eab52967b1193063b8374..2fbef43929b944c95804f823142bbad546bff149 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -1,7 +1,6 @@
 console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`);
 
 import "./assets/stylesheets/hub.scss";
-import queryString from "query-string";
 
 import { patchWebGLRenderingContext } from "./utils/webgl";
 patchWebGLRenderingContext();
@@ -101,14 +100,10 @@ window.APP.RENDER_ORDER = {
 };
 const store = window.APP.store;
 
-const qs = queryString.parse(location.search);
+const qs = new URLSearchParams(location.search);
 const isMobile = AFRAME.utils.device.isMobile();
 
-if (qs.quality) {
-  window.APP.quality = qs.quality;
-} else {
-  window.APP.quality = isMobile ? "low" : "high";
-}
+window.APP.quality = qs.get("quality") || isMobile ? "low" : "high";
 
 import "aframe-physics-system";
 import "aframe-physics-extras";
@@ -136,9 +131,9 @@ import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-cap
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
 function qsTruthy(param) {
-  const val = qs[param];
-  // if the param exists but is not set (e.g. "?foo&bar"), its value is null.
-  return val === null || /1|on|true/i.test(val);
+  const val = qs.get(param);
+  // if the param exists but is not set (e.g. "?foo&bar"), its value is the empty string.
+  return val === "" || /1|on|true/i.test(val);
 }
 
 const isBotMode = qsTruthy("bot");
@@ -167,7 +162,7 @@ store.init();
 
 function mountUI(scene, props = {}) {
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
-  const forcedVREntryType = qs.vr_entry_type || null;
+  const forcedVREntryType = qs.get("vr_entry_type");
   const enableScreenSharing = qsTruthy("enable_screen_sharing");
   const htmlPrefix = document.body.dataset.htmlPrefix || "";
   const showProfileEntry = !store.state.activity.hasChangedName;
@@ -269,7 +264,7 @@ const onReady = async () => {
     applyProfileOnPlayerRig();
     store.addEventListener("statechanged", applyProfileOnPlayerRig);
 
-    const avatarScale = parseInt(qs.avatar_scale, 10);
+    const avatarScale = parseInt(qs.get("avatar_scale"), 10);
 
     if (avatarScale) {
       playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale });
@@ -433,9 +428,9 @@ const onReady = async () => {
     return;
   }
 
-  if (qs.required_version && process.env.BUILD_VERSION) {
+  if (qs.get("required_version") && process.env.BUILD_VERSION) {
     const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)"
-    if (qs.required_version !== buildNumber) {
+    if (qs.get("required_version") !== buildNumber) {
       remountUI({ roomUnavailableReason: "version_mismatch" });
       setTimeout(() => document.location.reload(), 5000);
       exitScene();
@@ -485,9 +480,9 @@ const onReady = async () => {
     }
   };
 
-  if (qs.room) {
+  if (qs.has("room")) {
     // If ?room is set, this is `yarn start`, so just use a default environment and query string room.
-    setRoom(qs.room || "default");
+    setRoom(qs.get("room") || "default");
     initialEnvironmentEl.setAttribute("gltf-bundle", {
       src: DEFAULT_ENVIRONMENT_URL
     });
@@ -495,7 +490,7 @@ const onReady = async () => {
   }
 
   // Connect to reticulum over phoenix channels to get hub info.
-  const hubId = qs.hub_id || document.location.pathname.substring(1).split("/")[0];
+  const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0];
   console.log(`Hub ID: ${hubId}`);
 
   const socket = connectToReticulum();
diff --git a/src/index.js b/src/index.js
index d0b5c4557cda8a0e408ef01c6e5fa2ae680a01ed..f61c18337e40519a46693bf09607c105be58efae 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,15 +4,14 @@ import ReactDOM from "react-dom";
 import registerTelemetry from "./telemetry";
 import HomeRoot from "./react-components/home-root";
 import InfoDialog from "./react-components/info-dialog.js";
-import queryString from "query-string";
 
-const qs = queryString.parse(location.search);
+const qs = new URLSearchParams(location.search);
 registerTelemetry();
 
 ReactDOM.render(
   <HomeRoot
-    initialEnvironment={qs.initial_environment}
-    dialogType={qs.list_signup ? InfoDialog.dialogTypes.updates : null}
+    initialEnvironment={qs.get("initial_environment")}
+    dialogType={qs.has("list_signup") ? InfoDialog.dialogTypes.updates : null}
   />,
   document.getElementById("home-root")
 );
diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js
index e32a02d3eca931e112eb976b3c1cdcafc55f6dff..93ae3cc44237d8938443b59b9ad6b2990a7c9029 100644
--- a/src/react-components/2d-hud.js
+++ b/src/react-components/2d-hud.js
@@ -1,18 +1,17 @@
 import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import queryString from "query-string";
 
 import styles from "../assets/stylesheets/2d-hud.scss";
 
 import FontAwesomeIcon from "@fortawesome/react-fontawesome";
 import faPlus from "@fortawesome/fontawesome-free-solid/faPlus";
 
-const qs = queryString.parse(location.search);
+const qs = new URLSearchParams(location.search);
 function qsTruthy(param) {
-  const val = qs[param];
-  // if the param exists but is not set (e.g. "?foo&bar"), its value is null.
-  return val === null || /1|on|true/i.test(val);
+  const val = qs.get(param);
+  // if the param exists but is not set (e.g. "?foo&bar"), its value is the empty string.
+  return val === "" || /1|on|true/i.test(val);
 }
 const enableMediaTools = qsTruthy("mediaTools");
 
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index ce6358cb8ca3063f11fbb35894e7e69f99469840..e9ff7f529c58672f789bac21b295b70cebb8757b 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -2,7 +2,6 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import classNames from "classnames";
 import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect";
-import queryString from "query-string";
 import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
 import en from "react-intl/locale-data/en";
 import MovingAverage from "moving-average";
@@ -281,12 +280,12 @@ class UIRoot extends Component {
 
       // We are not in mobile chrome, so launch into chrome via an Intent URL
       const location = window.location;
-      const qs = queryString.parse(location.search);
-      qs.vr_entry_type = "daydream"; // Auto-choose 'daydream' after landing in chrome
+      const qs = new URLSearchParams(location.search);
+      qs.set("vr_entry_type", "daydream"); // Auto-choose 'daydream' after landing in chrome
 
       const intentUrl =
-        `intent://${location.host}${location.pathname || ""}?` +
-        `${queryString.stringify(qs)}#Intent;scheme=${(location.protocol || "http:").replace(":", "")};` +
+        `intent://${location.host}${location.pathname}?` +
+        `${qs}#Intent;scheme=${location.protocol.replace(":", "")};` +
         `action=android.intent.action.VIEW;package=com.android.chrome;end;`;
 
       window.location = intentUrl;
diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js
index 3c20f8db96aa261e4d8dd27d727b89b499728e53..91317a70d4ea0563842954ef407631c64a1ecacc 100644
--- a/src/utils/phoenix-utils.js
+++ b/src/utils/phoenix-utils.js
@@ -1,15 +1,14 @@
-import queryString from "query-string";
 import uuid from "uuid/v4";
 import { Socket } from "phoenix";
 
 export function connectToReticulum() {
-  const qs = queryString.parse(location.search);
+  const qs = new URLSearchParams(location.search);
 
-  const socketProtocol = qs.phx_protocol || (document.location.protocol === "https:" ? "wss:" : "ws:");
+  const socketProtocol = qs.get("phx_protocol") || (document.location.protocol === "https:" ? "wss:" : "ws:");
   const [retHost, retPort] = (process.env.DEV_RETICULUM_SERVER || "").split(":");
   const isProd = process.env.NODE_ENV === "production";
-  const socketPort = qs.phx_port || (isProd ? document.location.port : retPort) || "443";
-  const socketHost = qs.phx_host || (isProd ? document.location.hostname : retHost) || "";
+  const socketPort = qs.get("phx_port") || (isProd ? document.location.port : retPort) || "443";
+  const socketHost = qs.get("phx_host") || (isProd ? document.location.hostname : retHost) || "";
   const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`;
   console.log(`Phoenix Socket URL: ${socketUrl}`);
 
diff --git a/yarn.lock b/yarn.lock
index ccf9e5ea5c7dc4786625753d1e72ebc27f9d67f7..4bd30225dc8626c30c5a590523bcd70c9626f655 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3971,16 +3971,6 @@ he@1.1.x:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
 
-history@^4.7.2:
-  version "4.7.2"
-  resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
-  dependencies:
-    invariant "^2.2.1"
-    loose-envify "^1.2.0"
-    resolve-pathname "^2.2.0"
-    value-equal "^0.4.0"
-    warning "^3.0.0"
-
 hmac-drbg@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -3993,10 +3983,6 @@ hoek@2.x.x:
   version "2.16.3"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
 
-hoist-non-react-statics@^2.3.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
-
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -4346,7 +4332,7 @@ intl-relativeformat@^2.0.0:
   dependencies:
     intl-messageformat "^2.0.0"
 
-invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
+invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.2:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688"
   dependencies:
@@ -5058,7 +5044,7 @@ loglevelnext@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e"
 
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
   dependencies:
@@ -6071,12 +6057,6 @@ path-to-regexp@0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
 
-path-to-regexp@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
-  dependencies:
-    isarray "0.0.1"
-
 path-type@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -6487,7 +6467,7 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.4, prop-types@^15.6.0:
+prop-types@^15.6.0:
   version "15.6.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
   dependencies:
@@ -6566,14 +6546,6 @@ query-string@^4.1.0, query-string@^4.2.3:
     object-assign "^4.1.0"
     strict-uri-encode "^1.0.0"
 
-query-string@^5.0.1:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.0.tgz#9583b15fd1307f899e973ed418886426a9976469"
-  dependencies:
-    decode-uri-component "^0.2.0"
-    object-assign "^4.1.0"
-    strict-uri-encode "^1.0.0"
-
 querystring-es3@^0.2.0, querystring-es3@~0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6654,29 +6626,6 @@ react-intl@^2.4.0:
     intl-relativeformat "^2.0.0"
     invariant "^2.1.1"
 
-react-router-dom@^4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
-  dependencies:
-    history "^4.7.2"
-    invariant "^2.2.2"
-    loose-envify "^1.3.1"
-    prop-types "^15.5.4"
-    react-router "^4.2.0"
-    warning "^3.0.0"
-
-react-router@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"
-  dependencies:
-    history "^4.7.2"
-    hoist-non-react-statics "^2.3.0"
-    invariant "^2.2.2"
-    loose-envify "^1.3.1"
-    path-to-regexp "^1.7.0"
-    prop-types "^15.5.4"
-    warning "^3.0.0"
-
 react@^16.1.1:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
@@ -7043,10 +6992,6 @@ resolve-from@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
 
-resolve-pathname@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879"
-
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -8357,10 +8302,6 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "~1.0.0"
     spdx-expression-parse "~1.0.0"
 
-value-equal@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
-
 vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -8413,12 +8354,6 @@ vm-browserify@0.0.4, vm-browserify@~0.0.1:
   dependencies:
     indexof "0.0.1"
 
-warning@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
-  dependencies:
-    loose-envify "^1.0.0"
-
 watchify-middleware@^1.6.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/watchify-middleware/-/watchify-middleware-1.8.0.tgz#8f7cb9c528022be8525a7e066c10e2fd8c544be6"