async function createApp(options) {
  const {storage, useBodyParser} = options;

  log('[createApp] initializing storage method');
  const storageMethod = StorageMethod.from(storage);
  await storageMethod.initialize(storage);

  log('[createApp] creating express app');
  const context = {storageMethod, options};
  const app = express();
  if (options.logLevel !== 'silent') app.use(morgan('short'));

  // While LHCI should be served behind nginx/apache that handles compression, it won't always be.
  app.use(compression());

  if (typeof useBodyParser === 'undefined' || useBodyParser) {
    // 1. Optional if you want to overwrite by other middleware like koa or fastify
    // 2. Support large payloads because LHRs are big.
    // 3. Support JSON primitives because `PUT /builds/<id>/lifecycle "sealed"`
    app.use(bodyParser.json({limit: '10mb', strict: false}));
  }

  // Add a health check route before auth
  app.use('/healthz', (_, res) => res.send('healthy'));

  // The rest of the server can be protected by HTTP Basic Authentication
  const authMiddleware = createBasicAuthMiddleware(context);
  if (authMiddleware) app.use(authMiddleware);

  app.get('/', (_, res) => res.redirect('/app'));
  app.use('/version', (_, res) => res.send(version));
  app.use('/v1/projects', createProjectsRouter(context));
  app.use('/v1/viewer', createViewerRouter(options));
  app.use('/app', express.static(DIST_FOLDER));
  app.get('/app/*', (_, res) => res.sendFile(path.join(DIST_FOLDER, 'index.html')));
  app.use(errorMiddleware);

  log('[createApp] launching cron jobs');
  startPsiCollectCron(storageMethod, options);
  startDeleteOldBuildsCron(storageMethod, options);

  return {app, storageMethod};
}
@param {LHCI.ServerCommand.Options} options
@return {Promise<{app: Parameters<typeof createHttpServer>[1], storageMethod: StorageMethod}>}