diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js
--- a/keyserver/src/keyserver.js
+++ b/keyserver/src/keyserver.js
@@ -62,7 +62,8 @@
     initENSCache(),
   ]);
 
-  const keyserverBaseRoutePath = getKeyserverURLFacts()?.baseRoutePath;
+  const keyserverURLFacts = getKeyserverURLFacts();
+  const keyserverBaseRoutePath = keyserverURLFacts?.baseRoutePath;
   const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath;
   const webAppURLFacts = getWebAppURLFacts();
   const webAppBaseRoutePath = webAppURLFacts?.baseRoutePath;
@@ -145,14 +146,46 @@
     server.use(express.json({ limit: '250mb' }));
     server.use(cookieParser());
 
-    const setupAppRouter = router => {
-      if (areEndpointMetricsEnabled) {
-        router.use(logEndpointMetrics);
-      }
-      router.use('/images', express.static('images'));
-      router.use('/fonts', express.static('fonts'));
-      router.use('/misc', express.static('misc'));
-      router.use(
+    // Note - the order of router declarations matters. On prod we have
+    // keyserverBaseRoutePath configured to '/', which means it's a catch-all.
+    // If we call server.use on keyserverRouter first, it will catch all
+    // requests and prevent webAppRouter and landingRouter from working
+    // correctly. So we make sure that keyserverRouter goes last
+
+    server.get('/invite/:secret', inviteResponder);
+
+    if (landingBaseRoutePath) {
+      const landingRouter = express.Router<$Request, $Response>();
+      landingRouter.get('/invite/:secret', inviteResponder);
+      landingRouter.use(
+        '/.well-known',
+        express.static(
+          '.well-known',
+          // Necessary for apple-app-site-association file
+          {
+            setHeaders: res =>
+              res.setHeader('Content-Type', 'application/json'),
+          },
+        ),
+      );
+      landingRouter.use('/images', express.static('images'));
+      landingRouter.use('/fonts', express.static('fonts'));
+      landingRouter.use(
+        '/compiled',
+        express.static('landing_compiled', compiledFolderOptions),
+      );
+      landingRouter.use('/', express.static('landing_icons'));
+      landingRouter.post('/subscribe_email', emailSubscriptionResponder);
+      landingRouter.get('*', landingHandler);
+      server.use(landingBaseRoutePath, landingRouter);
+    }
+
+    if (webAppBaseRoutePath) {
+      const webAppRouter = express.Router<$Request, $Response>();
+      webAppRouter.use('/images', express.static('images'));
+      webAppRouter.use('/fonts', express.static('fonts'));
+      webAppRouter.use('/misc', express.static('misc'));
+      webAppRouter.use(
         '/.well-known',
         express.static(
           '.well-known',
@@ -163,92 +196,70 @@
           },
         ),
       );
-      router.use(
+      webAppRouter.use(
         '/compiled',
         express.static('app_compiled', compiledFolderOptions),
       );
-      router.use('/', express.static('icons'));
+      webAppRouter.use('/', express.static('icons'));
+
+      webAppRouter.get('/invite/:secret', inviteResponder);
+
+      webAppRouter.get('/worker/:worker', webWorkerResponder);
+
+      if (keyserverURLFacts) {
+        webAppRouter.get(
+          '/upload/:uploadID/:secret',
+          (req: $Request, res: $Response) => {
+            const { uploadID, secret } = req.params;
+            const url = `${keyserverURLFacts.baseDomain}${keyserverURLFacts.basePath}upload/${uploadID}/${secret}`;
+            res.redirect(url);
+          },
+        );
+      }
+
+      webAppRouter.get('*', htmlHandler(websiteResponder));
+
+      server.use(webAppBaseRoutePath, webAppRouter);
+    }
+
+    if (keyserverBaseRoutePath) {
+      const keyserverRouter = express.Router<$Request, $Response>();
+      if (areEndpointMetricsEnabled) {
+        keyserverRouter.use(logEndpointMetrics);
+      }
+      if (keyserverCorsOptions) {
+        keyserverRouter.use(cors(keyserverCorsOptions));
+      }
 
       for (const endpoint in jsonEndpoints) {
         // $FlowFixMe Flow thinks endpoint is string
         const responder = jsonEndpoints[endpoint];
         const expectCookieInvalidation = endpoint === 'log_out';
-        router.post(
+        keyserverRouter.post(
           `/${endpoint}`,
           jsonHandler(responder, expectCookieInvalidation),
         );
       }
 
-      router.get(
+      keyserverRouter.get(
         '/download_error_report/:reportID',
         downloadHandler(errorReportDownloadResponder),
       );
-      router.get(
+      keyserverRouter.get(
         '/upload/:uploadID/:secret',
         downloadHandler(uploadDownloadResponder),
       );
 
-      router.get('/invite/:secret', inviteResponder);
-
       // $FlowFixMe express-ws has side effects that can't be typed
-      router.ws('/ws', onConnection);
-      router.get('/worker/:worker', webWorkerResponder);
-      router.get('*', htmlHandler(websiteResponder));
+      keyserverRouter.ws('/ws', onConnection);
 
-      router.post(
+      keyserverRouter.post(
         '/upload_multimedia',
         multerProcessor,
         uploadHandler(multimediaUploadResponder),
       );
-    };
-
-    // Note - the order of router declarations matters. On prod we have
-    // squadCalBaseRoutePath configured to '/', which means it's a catch-all. If
-    // we call server.use on squadCalRouter first, it will catch all requests
-    // and prevent commAppRouter and landingRouter from working correctly. So we
-    // make sure that squadCalRouter goes last
-
-    server.get('/invite/:secret', inviteResponder);
-
-    if (landingBaseRoutePath) {
-      const landingRouter = express.Router();
-      landingRouter.get('/invite/:secret', inviteResponder);
-      landingRouter.use(
-        '/.well-known',
-        express.static(
-          '.well-known',
-          // Necessary for apple-app-site-association file
-          {
-            setHeaders: res =>
-              res.setHeader('Content-Type', 'application/json'),
-          },
-        ),
-      );
-      landingRouter.use('/images', express.static('images'));
-      landingRouter.use('/fonts', express.static('fonts'));
-      landingRouter.use(
-        '/compiled',
-        express.static('landing_compiled', compiledFolderOptions),
-      );
-      landingRouter.use('/', express.static('landing_icons'));
-      landingRouter.post('/subscribe_email', emailSubscriptionResponder);
-      landingRouter.get('*', landingHandler);
-      server.use(landingBaseRoutePath, landingRouter);
-    }
-
-    if (webAppBaseRoutePath) {
-      const commAppRouter = express.Router();
-      setupAppRouter(commAppRouter);
-      server.use(webAppBaseRoutePath, commAppRouter);
-    }
 
-    if (keyserverBaseRoutePath) {
-      const squadCalRouter = express.Router();
-      if (keyserverCorsOptions) {
-        squadCalRouter.use(cors(keyserverCorsOptions));
-      }
-      setupAppRouter(squadCalRouter);
-      server.use(keyserverBaseRoutePath, squadCalRouter);
+      server.use(keyserverBaseRoutePath, keyserverRouter);
     }
 
     if (isDev && webAppURLFacts) {