Let’s laser-focus SN on the exact spots to fix so uploads succeed and previews render.

🔧 What broke & where to look
1) Firebase bucket name is wrong (404)

In services/firebaseStorage.ts you set the bucket to projectId.firebasestorage.app.
For the Admin SDK the default bucket is projectId.appspot.com. That mismatch causes the 404 “bucket doesn’t exist”.

Fix (minimal):

// services/firebaseStorage.ts
// change both occurrences
const bucketName = `${projectId}.appspot.com`; // ✅ not .firebasestorage.app
// and keep using getStorage(app).bucket(bucketName)


Also, when saving, use the actual mime type:

await file.save(buffer, {
  metadata: { contentType: mimeType /* pass through from upload */, cacheControl: 'public, max-age=31536000' },
});

2) Preview upload path is missing the bucket prefix (treats “stock” as a bucket)

In routes/stock-library.ts, when you upload the watermarked preview, you call:

const previewPath = objPathPreview(dbRow.id, file.filename);
// previewPath === "stock/preview/{id}/{filename}"  // ❌ no bucket here
await uploadToObjectStorage(previewPath, wmBuf, file.mimetype);


uploadToObjectStorage expects a full object path like /<bucket>/<folder>/…. Passing stock/… makes it try to use bucket = "stock" → upload fails silently, then your DB row exists with no preview.

Fix (minimal): build the cloud path with your first public search path.

function objPathPreview(dbId: string, filename: string): string {
  const publicPath = objectStorage.getPublicObjectSearchPaths()[0]; // e.g. "/my-bucket/public"
  return `${publicPath}/stock/preview/${dbId}/${filename}`; // ✅ includes bucket + base folder
}


Then the existing call works:

const previewPath = objPathPreview(dbRow.id, file.filename);
await uploadToObjectStorage(previewPath, wmBuf, file.mimetype);

3) Preview lookup prefix missing base folder (lists the wrong location)

In the GET routes (/:id/preview and /:id/download) you list files with:

const bucket = objectStorageClient.bucket(searchPath.split('/')[1]);
const prefix = `stock/preview/${id}/`; // ❌ missing base folder from searchPath
const [files] = await bucket.getFiles({ prefix });


If your PUBLIC_OBJECT_SEARCH_PATHS is /my-bucket/public, the correct prefix must include public/….

Fix (minimal):

for (const searchPath of objectStorage.getPublicObjectSearchPaths()) {
  const parts = searchPath.split('/');         // ["", "my-bucket", "public"]
  const bucketName = parts[1];
  const basePrefix = parts.slice(2).join('/'); // "public"
  const prefix = `${basePrefix}/stock/preview/${id}/`; // ✅ "public/stock/preview/{id}/"
  const bucket = objectStorageClient.bucket(bucketName);
  const [files] = await bucket.getFiles({ prefix });
  ...
}

4) Env vars must be set (or you’ll get hard errors)

ObjectStorageService throws if these aren’t present:

PUBLIC_OBJECT_SEARCH_PATHS → set to something like "/my-bucket/public".

PRIVATE_OBJECT_DIR → "/my-bucket/private".
Make sure both exist in your Replit secrets.

✂️ Drop-in patch snippets (for SN)
A) services/firebaseStorage.ts
// …inside getFirebaseAdmin()
const serviceAccountData = JSON.parse(serviceAccount);
const projectId = serviceAccountData.project_id;

const adminApp = initializeApp({
  credential: cert(serviceAccountData),
  storageBucket: `${projectId}.appspot.com`, // ✅
});

// …when saving:
export async function uploadOriginalToFirebase(
  buffer: Buffer,
  filename: string,
  photoId: string,
  mimeType: string = 'image/png' // pass from upload
) {
  const app = getFirebaseAdmin();
  const serviceAccountData = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY!);
  const projectId = serviceAccountData.project_id;
  const bucketName = `${projectId}.appspot.com`; // ✅

  const bucket = getStorage(app).bucket(bucketName);
  const file = bucket.file(`stock/originals/${photoId}/${filename}`);

  await file.save(buffer, {
    metadata: { contentType: mimeType, cacheControl: 'public, max-age=31536000' },
  });

  const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 365*24*60*60*1000 });
  return url;
}

B) routes/stock-library.ts (two changes)

Build the preview cloud path with the public search path:

function objPathPreview(dbId: string, filename: string): string {
  const publicPath = objectStorage.getPublicObjectSearchPaths()[0]; // "/bucket/base"
  return `${publicPath}/stock/preview/${dbId}/${filename}`;
}


Use the correct prefix when listing previews:

for (const searchPath of objectStorage.getPublicObjectSearchPaths()) {
  const parts = searchPath.split('/');
  const bucketName = parts[1];
  const basePrefix = parts.slice(2).join('/'); // eg "public"
  const prefix = `${basePrefix}/stock/preview/${id}/`; // ✅
  const bucket = objectStorageClient.bucket(bucketName);
  const [files] = await bucket.getFiles({ prefix });
  if (files.length) { file = files[0]; break; }
}


(Apply the same prefix logic in both /:id/preview and /:id/download for the unlicensed path.)

🧪 Quick test after patch

Confirm secrets:

PUBLIC_OBJECT_SEARCH_PATHS=/your-bucket/public

PRIVATE_OBJECT_DIR=/your-bucket/private

FIREBASE_SERVICE_ACCOUNT_KEY=… (valid service account JSON)

Upload a PNG and a JPG (to verify mime handling).

Ensure the item appears in your JSON DB and /api/stock/:id/preview streams the watermarked image.