here’s a tight prompt you can paste to Replit to make certain mockups are set up identically to stock photos (same upload → watermark → storage → preview/download/entitlement → listing). I also included full files so it’s one-shot.

✅ Replit prompt (copy-paste)

Mirror the Stock Photos pipeline for Mockups.
Do not change watermark/storage code; just duplicate the flow under a separate mockups namespace.
Implement:

Backend

Create routes/mockupLibraryRoutes.js with the exact same logic as stock photos, but using:

storage keys: mockups/original/{id}/{filename} and mockups/preview/{id}/{filename}

routes:

POST /api/mockups/upload

GET /api/mockups/:id/preview

GET /api/mockups/:id/download (uses the same entitlements/quota logic)

GET /api/mockups/:id/entitlement

GET /api/mockups/list → returns array of { id, name, mimeType, code, previewUrl, createdAt }

In server.js, add:

app.use("/api/mockups", mockupLibraryRoutes) after the Stripe webhook + express.json() order we already use.

Alias the existing front-end path by adding GET /api/stock/mockups that simply proxies/returns the same JSON as /api/mockups/list (so no frontend change is required).

Frontend
3) Update the Mockups page to mirror Stock Photos behavior:

Fetch from /api/stock/mockups (kept for compatibility).

Button logic is conditional using the same entitlement endpoint:

If licensed → Download

Else if quotaRemaining>0 → Download (uses 1 credit)

Else → Purchase $6.99 (add to cart with itemType: "stock_mockup").

In the card + modal header show title IBrandBiz | #XXXXXXXXXX and File type: PNG/JPG, deriving the 10-digit code like photos.

Keep everything else unchanged.

Backend — routes/mockupLibraryRoutes.js (new)
// routes/mockupLibraryRoutes.js
// Mockups upload/serve pipeline (mirrors stock photos)

const path = require("path");
const fs = require("fs/promises");
const express = require("express");
const multer = require("multer");
const { applyWatermark, toTenDigitCode } = require("../services/watermark");
const ObjectStorageService = require("../services/ObjectStorageService");
const {
  hasLicense,
  grantLicenses,
  getTotalRemaining,
  decrementOne,
} = require("../services/entitlements");

const router = express.Router();

// temp disk
const uploadDir = path.join(process.cwd(), "uploads", "mockups");
const storage = multer.diskStorage({
  destination: async (_req, _file, cb) => {
    await fs.mkdir(uploadDir, { recursive: true });
    cb(null, uploadDir);
  },
  filename: (_req, file, cb) => {
    const stamp = Date.now();
    const safe = file.originalname.replace(/\s+/g, "_");
    cb(null, `${stamp}_${safe}`);
  },
});
const upload = multer({ storage, limits: { fileSize: 25 * 1024 * 1024 } });

// object keys
const originalKey = (id, filename) => `mockups/original/${id}/${filename}`;
const previewKey  = (id, filename) => `mockups/preview/${id}/${filename}`;

// mock “DB insert”
async function insertMockupRecord({ originalName, mimeType, uploaderId }) {
  const id = Math.floor(Date.now() / 1000).toString(36) + Math.floor(Math.random()*1e6).toString(36);
  return { id, originalName, mimeType, uploaderId, createdAt: Date.now() };
}

// ---- Upload (admin) --------------------------------------------------------
router.post("/upload", upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "No file" });

    const row = await insertMockupRecord({
      originalName: req.file.originalname,
      mimeType: req.file.mimetype,
      uploaderId: req.user?.id || "admin",
    });

    const buf = await fs.readFile(req.file.path);
    // store original (private)
    await ObjectStorageService.putObject(originalKey(row.id, req.file.filename), buf, req.file.mimetype, { privacy: "private" });
    // watermarked preview (public)
    const wm = await applyWatermark(buf, { stockSeed: row.id, opacity: 0.15 });
    await ObjectStorageService.putObject(previewKey(row.id, req.file.filename), wm, req.file.mimetype, { privacy: "public" });

    await fs.unlink(req.file.path).catch(() => {});
    const previewUrl = await ObjectStorageService.getPublicUrl(previewKey(row.id, req.file.filename));

    res.json({
      id: row.id,
      code: toTenDigitCode(row.id),
      name: req.file.originalname,
      mimeType: req.file.mimetype,
      previewUrl,
      createdAt: row.createdAt,
    });
  } catch (e) {
    console.error("mockups upload error", e);
    res.status(500).json({ error: "upload_failed" });
  }
});

// ---- List (for UI) ---------------------------------------------------------
router.get("/list", async (_req, res) => {
  try {
    const files = await ObjectStorageService.listPrefix("mockups/preview/");
    const items = await Promise.all(
      files.map(async (f) => {
        // key: mockups/preview/{id}/{filename}
        const parts = f.key.split("/");
        const id = parts[2];
        return {
          id,
          name: parts[3],
          mimeType: f.contentType || "image/png",
          code: toTenDigitCode(id),
          previewUrl: await ObjectStorageService.getPublicUrl(f.key),
          createdAt: Date.now(),
          tags: [],
          category: "mockup",
        };
      })
    );
    res.json({ mockups: items });
  } catch (e) {
    console.error("mockups list error", e);
    res.status(500).json({ error: "list_failed" });
  }
});

// ---- Preview (always watermarked) ------------------------------------------
router.get("/:id/preview", async (req, res) => {
  try {
    const { id } = req.params;
    const list = await ObjectStorageService.listPrefix(`mockups/preview/${id}/`);
    if (!list.length) return res.status(404).send("Not found");
    const key = list[0].key;
    const stream = await ObjectStorageService.getStream(key);
    res.setHeader("Content-Type", list[0].contentType || "image/png");
    stream.pipe(res);
  } catch (e) {
    console.error(e);
    res.status(500).send("preview_failed");
  }
});

// ---- Download (license/quota gate) -----------------------------------------
router.get("/:id/download", async (req, res) => {
  try {
    const { id } = req.params;
    const userId = req.user?.id || null;

    async function streamOriginal() {
      const list = await ObjectStorageService.listPrefix(`mockups/original/${id}/`);
      if (!list.length) return res.status(404).send("Original not found");
      const key = list[0].key;
      const stream = await ObjectStorageService.getStream(key);
      res.setHeader("Content-Disposition", `attachment; filename="IBrandBiz_Mockup_${id}.png"`);
      res.setHeader("Content-Type", list[0].contentType || "image/png");
      return stream.pipe(res);
    }
    async function streamPreview(extra = {}) {
      const list = await ObjectStorageService.listPrefix(`mockups/preview/${id}/`);
      if (!list.length) return res.status(404).send("Preview not found");
      const key = list[0].key;
      const stream = await ObjectStorageService.getStream(key);
      for (const [k, v] of Object.entries(extra)) res.setHeader(k, v);
      res.setHeader("Content-Type", list[0].contentType || "image/png");
      return stream.pipe(res);
    }

    if (!userId) return streamPreview({ "X-Quota-Remaining": "0", "X-License": "none" });

    if (hasLicense(userId, id)) {
      res.setHeader("X-License", "owned");
      return streamOriginal();
    }
    const remaining = getTotalRemaining(userId);
    if (remaining > 0 && decrementOne(userId)) {
      grantLicenses(userId, [id]);
      res.setHeader("X-Quota-Remaining", String(remaining - 1));
      res.setHeader("X-License", "newly-granted");
      return streamOriginal();
    }
    return streamPreview({ "X-Quota-Remaining": "0", "X-License": "none" });
  } catch (e) {
    console.error(e);
    res.status(500).send("download_failed");
  }
});

// ---- Entitlement status -----------------------------------------------------
router.get("/:id/entitlement", async (req, res) => {
  try {
    const { id } = req.params;
    const userId = req.user?.id || null;
    const licensed = userId ? hasLicense(userId, id) : false;
    const quotaRemaining = userId ? getTotalRemaining(userId) : 0;
    res.json({ licensed, quotaRemaining, canDownloadOriginal: licensed || quotaRemaining > 0 });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: "entitlement_failed" });
  }
});

module.exports = router;

server.js wiring (add)
const mockupLibraryRoutes = require("./routes/mockupLibraryRoutes");

// keep webhook before json:
app.use("/api/stripe", stripeWebhook);
app.use(express.json());

app.use("/api/checkout", checkoutRoutes);
app.use("/api/stock", stockLibraryRoutes);
app.use("/api/mockups", mockupLibraryRoutes);

// alias for existing frontend fetch:
app.get("/api/stock/mockups", async (req, res) => {
  // proxy list for compatibility
  req.url = "/list";
  mockupLibraryRoutes.handle(req, res);
});

Frontend — tiny Mockups page tweaks

Conditional button + entitlement (same as Stock Photos).

“IBrandBiz | #code” + “File type: …”.

// mockups page additions at top:
import { useCart } from "@/contexts/CartContext";
import { useEffect, useState } from "react";

type Ent = { licensed: boolean; quotaRemaining: number; canDownloadOriginal: boolean };
async function tenDigit(seed: string) {
  const enc = new TextEncoder().encode(seed);
  const buf = await crypto.subtle.digest("SHA-256", enc);
  const hex = [...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,"0")).join("");
  return hex.replace(/[a-f]/g, c => (parseInt(c,16)%10).toString()).slice(0,10).padEnd(10,"0");
}
const fileType = (x: Item) => (x?.name?.split(".").pop() || "").toUpperCase();

function useEntitlement(assetId?: string) {
  const [s, setS] = useState<Ent & {loading:boolean}>({licensed:false,quotaRemaining:0,canDownloadOriginal:false,loading:true});
  useEffect(()=>{ if(!assetId) return;
    setS(s=>({...s,loading:true}));
    fetch(`/api/mockups/${assetId}/entitlement`).then(r=>r.json()).then(d=>setS({...d,loading:false})).catch(()=>setS(s=>({...s,loading:false})));
  },[assetId]);
  return s;
}


Then inside the Mockups grid where you render each item, swap the overlay Download anchor for conditional logic:

const { addItem, hasItem } = useCart();

// before return
const [code, setCode] = useState<string>("");
useEffect(()=>{ tenDigit(item.id).then(setCode); }, [item.id]);
const ent = useEntitlement(item.id);

const onPurchase = () => {
  addItem({
    itemType: "stock_mockup",
    itemId: item.id,
    itemName: `IBrandBiz | #${code}`,
    itemPrice: 699,
    quantity: 1,
    metadata: { url: item.url, tags: item.tags, category: item.category }
  });
};

// in the card content area:
<h3 className="font-medium text-sm">
  IBrandBiz | #{code} <span className="text-xs text-muted-foreground">• File type: {fileType(item) || "—"}</span>
</h3>

// replace hover action with conditional:
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center opacity-0 group-hover:opacity-100">
  {ent.loading ? (
    <div className="bg-white/90 px-4 py-2 rounded-lg text-gray-700 text-sm">Checking…</div>
  ) : ent.licensed || ent.quotaRemaining > 0 ? (
    <a
      href={`/api/mockups/${item.id}/download`}
      className="bg-white/90 hover:bg-white text-gray-900 px-4 py-2 rounded-lg font-medium flex items-center gap-2"
      data-testid={`download-${item.id}`}
    >
      <Download className="h-4 w-4" />
      {ent.licensed ? "Download" : "Download (uses 1 credit)"}
    </a>
  ) : (
    <button
      onClick={onPurchase}
      className="bg-white/90 hover:bg-white text-gray-900 px-4 py-2 rounded-lg font-medium flex items-center gap-2"
    >
      $6.99 — Purchase
    </button>
  )}
</div>


If your cart’s hasItem is used, you can mirror the “In Cart” disable just like the Stock Photos page (use itemType: "stock_mockup").

TL;DR

Mockups now have their own upload/preview/download/entitlement routes and watermark just like photos.

Your existing frontend fetch to /api/stock/mockups keeps working via the alias.

Conditional buttons + code/file-type labels match the Stock Photos UX.