diff --git a/package.json b/package.json
index bc1aaa9b11f32f4fb4e0ac9dace7a52f2daf6459..efd7e143dd71a19af950365d5eb518abe09930c0 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,6 @@
     "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",
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..df91581bde6fd2dd6d2d4ba30ad4752e1b7cb8ec 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(":", "")};` +
+        `${qs.toString()}#Intent;scheme=${(location.protocol || "http:").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 a8a065d3a778de589997bf97124b24a13936f591..4bd30225dc8626c30c5a590523bcd70c9626f655 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6546,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"