diff --git a/package-lock.json b/package-lock.json
index b6cc7770d591bbbc02bab06845da018763d8669d..4688884d728cb3ecf1254ae0d3cf2de259860cbd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -517,7 +517,9 @@
       "requires": {
         "@tweenjs/tween.js": "^16.8.0",
         "browserify-css": "^0.8.2",
+        "debug": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a",
         "deep-assign": "^2.0.0",
+        "document-register-element": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90",
         "envify": "^3.4.1",
         "load-bmfont": "^1.2.3",
         "object-assign": "^4.0.1",
@@ -531,11 +533,7 @@
       "dependencies": {
         "debug": {
           "version": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a",
-          "from": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a"
-        },
-        "document-register-element": {
-          "version": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90",
-          "from": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90"
+          "from": "github:ngokevin/debug#noTimestamp"
         }
       }
     },
@@ -3793,6 +3791,10 @@
         "esutils": "^2.0.2"
       }
     },
+    "document-register-element": {
+      "version": "github:dmarcos/document-register-element#8ccc532b7f3744be954574caf3072a5fd260ca90",
+      "from": "github:dmarcos/document-register-element#8ccc532b7"
+    },
     "dom-converter": {
       "version": "0.1.4",
       "resolved": "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz",
@@ -4183,6 +4185,11 @@
         "es6-symbol": "^3.1.1"
       }
     },
+    "es6-promise": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+      "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y="
+    },
     "es6-symbol": {
       "version": "3.1.1",
       "resolved": "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz",
@@ -5254,14 +5261,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -5276,20 +5281,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -5406,8 +5408,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -5419,7 +5420,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -5434,7 +5434,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -5442,14 +5441,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -5468,7 +5465,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -5549,8 +5545,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5562,7 +5557,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5684,7 +5678,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -6696,6 +6689,11 @@
       "integrity": "sha1-Cpf7h2mG6AgcYxFg+PnziRV/AEM=",
       "dev": true
     },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+    },
     "import-lazy": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz",
@@ -7633,6 +7631,48 @@
         "array-includes": "^3.0.3"
       }
     },
+    "jszip": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+      "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+      "requires": {
+        "core-js": "~2.3.0",
+        "es6-promise": "~3.0.2",
+        "lie": "~3.1.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.0.6"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+          "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~1.0.6",
+            "string_decoder": "~0.10.x",
+            "util-deprecate": "~1.0.1"
+          }
+        }
+      }
+    },
     "keyv": {
       "version": "3.0.0",
       "resolved": "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz",
@@ -7691,6 +7731,14 @@
         "type-check": "~0.3.2"
       }
     },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
     "listr": {
       "version": "0.14.1",
       "resolved": "https://registry.yarnpkg.com/listr/-/listr-0.14.1.tgz",
@@ -9471,8 +9519,7 @@
     "pako": {
       "version": "1.0.6",
       "resolved": "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz",
-      "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg=",
-      "dev": true
+      "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg="
     },
     "parallel-transform": {
       "version": "1.1.0",
@@ -13108,8 +13155,7 @@
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-      "dev": true
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
     "util.promisify": {
       "version": "1.0.0",
diff --git a/package.json b/package.json
index 95e111f280ee44a55872d5b1e964626e63dccc8e..09ec5d4fae8461b61b25f679bd66933a338a92a1 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
     "event-target-shim": "^3.0.1",
     "form-urlencoded": "^2.0.4",
     "jsonschema": "^1.2.2",
+    "jszip": "^3.1.5",
     "moving-average": "^1.0.0",
     "naf-janus-adapter": "^0.10.1",
     "networked-aframe": "github:mozillareality/networked-aframe#mr-social-client/master",
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 5e54e729e38c6d2360059e04f8fa4cb827d7b86f..b5904f0fc36c9695908f56017772449ee99dd7e3 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -1,3 +1,5 @@
+import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js";
+
 const GLTFCache = {};
 
 AFRAME.GLTFModelPlus = {
@@ -181,14 +183,41 @@ function nextTick() {
   });
 }
 
-function cachedLoadGLTF(src, basePath, preferredTechnique, onProgress) {
-  // Load the gltf model from the cache if it exists.
+function getFilesFromSketchfabZip(src) {
+  return new Promise((resolve, reject) => {
+    const worker = new SketchfabZipWorker();
+    worker.onmessage = e => {
+      const [success, fileMapOrError] = e.data;
+      (success ? resolve : reject)(fileMapOrError);
+    };
+    worker.postMessage(src);
+  });
+}
+
+function cachedLoadGLTF(src, basePath, contentType, preferredTechnique, onProgress) {
   if (!GLTFCache[src]) {
-    GLTFCache[src] = new Promise((resolve, reject) => {
-      const gltfLoader = new THREE.GLTFLoader();
-      gltfLoader.path = basePath;
-      gltfLoader.preferredTechnique = preferredTechnique;
-      gltfLoader.load(src, resolve, onProgress, reject);
+    GLTFCache[src] = new Promise(async (resolve, reject) => {
+      try {
+        let gltfUrl = src;
+        let onLoad = resolve;
+        if (contentType === "model/gltf+zip") {
+          const fileMap = await getFilesFromSketchfabZip(src);
+          gltfUrl = fileMap["scene.gtlf"];
+          onLoad = model => {
+            // The GLTF is now cached as a THREE object, we can get rid of the original blobs
+            Object.keys(fileMap).forEach(URL.revokeObjectURL);
+            resolve(model);
+          };
+        }
+
+        const gltfLoader = new THREE.GLTFLoader();
+        gltfLoader.path = basePath;
+        gltfLoader.preferredTechnique = preferredTechnique;
+        gltfLoader.load(gltfUrl, onLoad, onProgress, reject);
+      } catch (e) {
+        reject(e);
+        delete GLTFCache[src];
+      }
     });
   }
   return GLTFCache[src].then(cloneGltf);
@@ -203,6 +232,7 @@ function cachedLoadGLTF(src, basePath, preferredTechnique, onProgress) {
 AFRAME.registerComponent("gltf-model-plus", {
   schema: {
     src: { type: "string" },
+    contentType: { type: "string" },
     basePath: { type: "string", default: undefined },
     inflate: { default: false }
   },
@@ -244,7 +274,7 @@ AFRAME.registerComponent("gltf-model-plus", {
       }
 
       const gltfPath = THREE.LoaderUtils.extractUrlBase(src);
-      const model = await cachedLoadGLTF(src, this.data.basePath, this.preferredTechnique);
+      const model = await cachedLoadGLTF(src, this.data.basePath, this.data.contentType, this.preferredTechnique);
 
       // If we started loading something else already
       // TODO: there should be a way to cancel loading instead
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index ef8ea23cc33f5fb077bddf8ce6d0f269b6d04a36..544d4443a4304e0ed1d3ad75523c89cafa46c979 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -1,5 +1,5 @@
 import { getBox, getScaleCoefficient } from "../utils/auto-box-collider";
-import { resolveFarsparkUrl } from "../utils/media-utils";
+import { resolveMedia } from "../utils/media-utils";
 
 const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type"));
 
@@ -53,7 +53,7 @@ AFRAME.registerComponent("media-loader", {
         this.setShapeAndScale(true);
       }, 100);
 
-      const { raw, origin, meta } = await resolveFarsparkUrl(url);
+      const { raw, origin, meta } = await resolveMedia(url);
       console.log("resolved", url, raw, origin, meta);
 
       const contentType = (meta && meta.expected_content_type) || (await fetchContentType(raw));
@@ -79,6 +79,7 @@ AFRAME.registerComponent("media-loader", {
         this.el.addEventListener("model-error", this.onError, { once: true });
         this.el.setAttribute("gltf-model-plus", {
           src: raw,
+          contentType,
           basePath: THREE.LoaderUtils.extractUrlBase(origin),
           inflate: true
         });
diff --git a/src/hub.html b/src/hub.html
index cdc64e06934826057a8996897e36890e2eea71fd..45a849b2fc267bb1b35cdf84fe8be64be50852a7 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -26,7 +26,7 @@
     <a-scene
         renderer="antialias: true"
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
-        physics="gravity: -6;"
+        physics="gravity: -6; debug: false;"
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         freeze-controller="toggleEvent: action_freeze"
         personal-space-bubble="debug: false;"
diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js
index e4f812b59dd04d2402905e97e8a6e0f29f4e85f9..e864bfc9fd979ef42b4fd4083cd67325e3465b3a 100644
--- a/src/utils/media-utils.js
+++ b/src/utils/media-utils.js
@@ -1,20 +1,25 @@
 const whitelistedHosts = [/^.*\.reticulum\.io$/, /^.*hubs\.mozilla\.com$/, /^hubs\.local$/];
 const isHostWhitelisted = hostname => !!whitelistedHosts.filter(r => r.test(hostname)).length;
-let resolveMediaUrl = "/api/v1/media";
+let mediaAPIEndpoint = "/api/v1/media";
 if (process.env.NODE_ENV === "development") {
-  resolveMediaUrl = `https://${process.env.DEV_RETICULUM_SERVER}${resolveMediaUrl}`;
+  mediaAPIEndpoint = `https://${process.env.DEV_RETICULUM_SERVER}${mediaAPIEndpoint}`;
 }
 
-export const resolveFarsparkUrl = async url => {
+const resolveMediaCache = new Map();
+export const resolveMedia = async url => {
   const parsedUrl = new URL(url);
-  if ((parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") || isHostWhitelisted(parsedUrl.hostname))
-    return { raw: url, origin: url };
+  if (resolveMediaCache.has(url)) return resolveMediaCache.get(url);
 
-  return await fetch(resolveMediaUrl, {
-    method: "POST",
-    headers: { "Content-Type": "application/json" },
-    body: JSON.stringify({ media: { url } })
-  }).then(r => r.json());
+  const resolved =
+    (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") || isHostWhitelisted(parsedUrl.hostname)
+      ? { raw: url, origin: url }
+      : await fetch(mediaAPIEndpoint, {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({ media: { url } })
+        }).then(r => r.json());
+  resolveMediaCache.set(url, resolved);
+  return resolved;
 };
 
 let interactableId = 0;
diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js
index 5e23d9250e1f917f1206398fa361115e4be77d28..d92e1773df03db6a76566333fd77867c1a49342f 100644
--- a/src/vendor/GLTFLoader.js
+++ b/src/vendor/GLTFLoader.js
@@ -10,7 +10,7 @@
  * @author netpro2k / https://github.com/netpro2k
  */
 
- import { resolveFarsparkUrl } from "../utils/media-utils"
+ import { resolveMedia } from "../utils/media-utils"
 
 THREE.GLTFLoader = ( function () {
 
@@ -40,7 +40,7 @@ THREE.GLTFLoader = ( function () {
 
 			loader.setResponseType( 'arraybuffer' );
 
-			var farsparkURL = (await resolveFarsparkUrl(url)).raw;
+			var farsparkURL = (await resolveMedia(url)).raw;
 
 			loader.load( farsparkURL, function ( data ) {
 
@@ -1623,7 +1623,7 @@ THREE.GLTFLoader = ( function () {
 
 		var options = this.options;
 
-		var farsparkURL = (await resolveFarsparkUrl(resolveURL(bufferDef.uri, options.path))).raw;
+		var farsparkURL = (await resolveMedia(resolveURL(bufferDef.uri, options.path))).raw;
 
 		return new Promise( function ( resolve, reject ) {
 
@@ -1823,7 +1823,7 @@ THREE.GLTFLoader = ( function () {
 
     var urlToLoad = resolveURL(sourceURI, options.path);
     if (!hasBufferView){
-      urlToLoad = (await resolveFarsparkUrl(urlToLoad)).raw;
+      urlToLoad = (await resolveMedia(urlToLoad)).raw;
     }
 
 		return Promise.resolve( sourceURI ).then( function ( sourceURI ) {
diff --git a/src/workers/gifparsing.worker.js b/src/workers/gifparsing.worker.js
index 643a95ab98e52c048ef551a6249e3ef937191389..1d1400cde35215bc65eba23863c8baa978bcb799 100644
--- a/src/workers/gifparsing.worker.js
+++ b/src/workers/gifparsing.worker.js
@@ -63,10 +63,12 @@ self.onmessage = e => {
     new Uint8Array(e.data),
     (delays, loopcnt, frames, disposals) => {
       self.postMessage([true, frames, delays, disposals]);
+      delete self.onmessage;
     },
     err => {
       console.error("Error in gif parsing worker", err);
       self.postMessage([false, err]);
+      delete self.onmessage;
     }
   );
 };
diff --git a/src/workers/sketchfab-zip.worker.js b/src/workers/sketchfab-zip.worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..9108cbb5597669d99f6c62bcfed4798946458fa1
--- /dev/null
+++ b/src/workers/sketchfab-zip.worker.js
@@ -0,0 +1,33 @@
+import JSZip from "jszip";
+
+async function fetchZipAndGetBlobs(src) {
+  const zip = await fetch(src)
+    .then(r => r.blob())
+    .then(JSZip.loadAsync);
+
+  // Rewrite any url refferences in the GLTF to blob urls
+  const fileMap = {};
+  const files = Object.values(zip.files);
+  const fileBlobs = await Promise.all(files.map(f => f.async("blob")));
+  for (let i = 0; i < fileBlobs.length; i++) {
+    fileMap[files[i].name] = URL.createObjectURL(fileBlobs[i]);
+  }
+
+  const gltfJson = JSON.parse(await zip.file("scene.gltf").async("text"));
+  gltfJson.buffers && gltfJson.buffers.forEach(b => (b.uri = fileMap[b.uri]));
+  gltfJson.images && gltfJson.images.forEach(i => (i.uri = fileMap[i.uri]));
+
+  fileMap["scene.gtlf"] = URL.createObjectURL(new Blob([JSON.stringify(gltfJson, null, 2)], { type: "text/plain" }));
+
+  return fileMap;
+}
+
+self.onmessage = async e => {
+  try {
+    const fileMap = await fetchZipAndGetBlobs(e.data);
+    self.postMessage([true, fileMap]);
+  } catch (e) {
+    self.postMessage([false, e.message]);
+  }
+  delete self.onmessage;
+};