Easiest reliable flow:

Server downloads the Firebase URL once, saves it (cache).

Viewer loads the cached copy from your domain.

Below is a tiny, drop-in pair of routes + minimal frontend hook. Works today, no new deps.

Server (Express) — add both routes
// top of server
import { createHash } from "node:crypto";
import { mkdir, stat } from "node:fs/promises";
import { createReadStream, createWriteStream } from "node:fs";
const CACHE_DIR = ".cache/pdfs"; await mkdir(CACHE_DIR, { recursive: true });
const ALLOWED = /^(https:\/\/(?:storage\.googleapis\.com|firebasestorage\.googleapis\.com|[^/]+\.firebasestorage\.app)\/)/i;

// POST /api/cache/pdf?u=<encoded Firebase URL>&filename=optional.pdf
app.post("/api/cache/pdf", async (req, res) => {
  try {
    const u = req.query.u; if (!u) return res.status(400).send("Missing ?u");
    const url = decodeURIComponent(String(u));
    if (!ALLOWED.test(url)) return res.status(400).send("Blocked URL");

    const id = createHash("sha1").update(url).digest("hex");
    const path = `${CACHE_DIR}/${id}.pdf`;

    try { await stat(path); return res.json({ id }); } catch {} // already cached

    const upstream = await fetch(url);
    if (!upstream.ok || !upstream.body) return res.status(502).send("Fetch failed");
    await new Promise((resolve, reject) => {
      const out = createWriteStream(path);
      out.on("finish", resolve).on("error", reject);
      // @ts-ignore
      require("node:stream").Readable.fromWeb(upstream.body).pipe(out);
    });
    res.json({ id });
  } catch (e) { res.status(502).send("Cache error"); }
});

// GET /api/preview/pdf/:id  → stream cached file inline
app.get("/api/preview/pdf/:id", async (req, res) => {
  try {
    const id = req.params.id;
    const path = `${CACHE_DIR}/${id}.pdf`;
    await stat(path);
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", `inline; filename="${id}.pdf"`);
    res.setHeader("Accept-Ranges", "bytes");
    if (req.headers.range) res.status(206);
    createReadStream(path, req.headers.range ? { start: Number((req.headers.range||"").match(/\d+/)?.[0]) } : {})
      .pipe(res);
  } catch { res.status(404).send("Not cached"); }
});


Notes

Allows only Google Storage/Firebase hosts (prevents open-proxy abuse).

Files live under ./.cache/pdfs. On Replit, this persists while the instance is alive; redeploy clears it (fine for a view cache).

Range support is basic but enough for the built-in PDF viewer.

Frontend — “download then display”
// given: template.pdfUrl (the signed Firebase URL)
async function getPreviewSrc(signedUrl: string) {
  const resp = await fetch(`/api/cache/pdf?u=${encodeURIComponent(signedUrl)}`, { method: "POST" });
  const { id } = await resp.json();
  return `/api/preview/pdf/${id}`;
}

// when opening the modal:
const src = await getPreviewSrc(template.pdfUrl);
setViewerSrc(src); // use in <iframe src={viewerSrc}/> or <object data={viewerSrc}/>


Keep your “Open in new tab” fallback pointing at the original Firebase URL for redundancy.