Yep—two ways to “download first, then display,” both safe. I’ll give you the cached-proxy (best UX) and the persist-to-bucket (long-term archive) options. You can ship the first in minutes.

Option A — Cached proxy (recommended)

Flow: first request pulls the Firebase URL server-side, saves it to a small local cache, then serves from your domain. Later requests hit the cache instantly.

Server (micro-diff)

Add to your Express server (extends the proxy I gave you).

// top-level (once)
import { createHash } from "node:crypto";
import { mkdir, stat } from "node:fs/promises";
import { createReadStream, createWriteStream } from "node:fs";
const { Readable } = await import("node:stream");
const CACHE_DIR = ".cache/pdfs"; await mkdir(CACHE_DIR, { recursive: true });

// GET /api/preview/pdf?u=<encodedFirebaseUrl>&filename=...&cache=1
app.get("/api/preview/pdf", async (req, res) => {
  try {
    const u = req.query.u; if (typeof u !== "string") return res.status(400).send("Missing url");
    const url = decodeURIComponent(u);
    if (!/^https:\/\/firebasestorage\.googleapis\.com\/v0\/b\//i.test(url)) return res.status(400).send("Blocked");
    const name = (req.query.filename || "document.pdf").toString();
    const headers = req.headers.range ? { Range: req.headers.range } : {};
    const hash = createHash("sha1").update(url).digest("hex");
    const path = `${CACHE_DIR}/${hash}.pdf`;

    // Cache hit
    try { await stat(path);
      res.setHeader("Content-Type","application/pdf");
      res.setHeader("Content-Disposition",`inline; filename="${name}"`);
      return createReadStream(path).pipe(res);
    } catch {}

    // Cache miss → fetch, tee to disk, then stream
    const upstream = await fetch(url, { headers });
    if (!upstream.body) return res.sendStatus(upstream.status);
    res.status(upstream.status);
    ["content-range","accept-ranges","etag","last-modified"]
      .forEach(h=>{const v=upstream.headers.get(h); if(v) res.setHeader(h,v);});
    res.setHeader("Content-Type","application/pdf");
    res.setHeader("Content-Disposition",`inline; filename="${name}"`);

    const file = createWriteStream(path);
    Readable.fromWeb(upstream.body).once("end",()=>file.end()).pipe(file);
    createReadStream(path).once("open",()=>{}).once("error",()=>{}); // pre-open
    file.on("finish", ()=> createReadStream(path).pipe(res));
  } catch { res.status(502).send("Upstream error"); }
});

Frontend (micro-diff)

Where you build the preview URL for the modal:

const proxied = `/api/preview/pdf?u=${encodeURIComponent(template.pdfUrl)}&filename=${encodeURIComponent(template.title || 'template.pdf')}&cache=1`;


Use proxied in your <object data=...> or <iframe src=...>.

Why this works: the file is now served same-origin from your app after being cached locally, so the browser happily renders it. First load fetches once; subsequent opens are instant.