Here’s the clean update so Replit can patch fast without breaking anything.

1) Storage path correction — put thumbs under /thumbs/
New canonical layout
templates/
  business-plan/
    docs/
      <slug>/
        v1.docx
    previews/
      <slug>/
        v1-preview.webp         (full card)
    thumbs/
      <slug>/
        v1-thumb.webp           (small list/grid)

Update Firestore doc pointers
"storagePaths": {
  "docx": "templates/business-plan/docs/business-plan-general-blank-template/v1.docx",
  "preview": "templates/business-plan/previews/business-plan-general-blank-template/v1-preview.webp",
  "thumb": "templates/business-plan/thumbs/business-plan-general-blank-template/v1-thumb.webp"
}

Storage rules (merged with your existing logos)
// storage.rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // LOGOS (unchanged)
    match /templates/logos/{uid}/{rest=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }

    // BUSINESS-PLAN (previews, thumbs, docs)
    match /templates/business-plan/docs/{rest=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }
    match /templates/business-plan/previews/{rest=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }
    match /templates/business-plan/thumbs/{rest=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }

    // Optional catch-all AFTER specifics
    match /templates/{rest=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }
  }
}

2) Atomic “version master” management

Goal: when promoting a version (e.g., v2) to master:

Demote all versions under that slug.

Set the promoted version isMaster: true.

Update the slug doc (isMaster: true, currentVersion: "v2").

(Optional) Update settings/templates.business-plan.masterSlug/masterVersion.

All in one transaction.

Firestore structure recap
templates/business-plan/types/{slug}            // slug doc (e.g., business-plan-general-blank-template)
templates/business-plan/types/{slug}/versions/{version}  // v1, v2, ...
settings/templates.business-plan                 // optional pointer

Firestore rules (unchanged logic, just ensure nested path is allowed)
// firestore.rules (excerpt)
rules_version = '2';
service cloud.firestore {
  function isAdmin() {
    return request.auth != null && request.auth.token.admin == true;
  }
  match /databases/{database}/documents {
    // Keep your existing logos collections...
    match /templates/{docId} {
      allow read: if true;
      allow create, update, delete: if isAdmin();
    }
    // Allow nested business-plan tree
    match /templates/{document=**} {
      allow read: if true;
      allow write: if isAdmin();
    }
    match /settings/{doc=**} {
      allow read: if true;
      allow write: if isAdmin();
    }
  }
}

Transaction helper (TypeScript, client or Cloud Function)
import {
  getFirestore, doc, collection, getDocs, query, where, runTransaction
} from "firebase/firestore";

type PromotePayload = {
  category: "business-plan";
  slug: string;          // e.g., "business-plan-general-blank-template"
  version: string;       // e.g., "v2"
};

export async function promoteVersionToMaster({ slug, version }: PromotePayload) {
  const db = getFirestore();

  const slugRef = doc(db, "templates/business-plan/types", slug);
  const versionsCol = collection(db, `templates/business-plan/types/${slug}/versions`);

  await runTransaction(db, async (tx) => {
    // 1) Make sure slug doc exists
    const slugSnap = await tx.get(slugRef);
    if (!slugSnap.exists()) {
      throw new Error(`Slug not found: ${slug}`);
    }

    // 2) Fetch all versions for the slug (in-transaction read)
    const allVersionsSnap = await tx.get(versionsCol);

    // 3) Demote ALL versions
    allVersionsSnap.forEach((verDoc) => {
      const verRef = doc(db, `templates/business-plan/types/${slug}/versions`, verDoc.id);
      tx.update(verRef, { isMaster: false });
    });

    // 4) Promote target version
    const targetRef = doc(db, `templates/business-plan/types/${slug}/versions`, version);
    const targetSnap = await tx.get(targetRef);
    if (!targetSnap.exists()) {
      throw new Error(`Version not found: ${slug}/${version}`);
    }
    tx.update(targetRef, { isMaster: true });

    // 5) Update slug doc pointers
    tx.update(slugRef, {
      isMaster: true,
      currentVersion: version,
      updatedAt: new Date()
    });

    // 6) (Optional) update global pointer
    const pointerRef = doc(db, "settings/templates.business-plan");
    // Use set with merge so doc can be created if missing
    tx.set(pointerRef, {
      masterSlug: slug,
      masterVersion: version,
      lastRotatedAt: new Date()
    }, { merge: true });
  });
}


Why this is atomic: all demotions + promotion + slug update happen in one transaction. If any step fails, none of the docs are committed—no “two masters” or “zero masters” state.

Bonus guard (optional): before promoting, verify targetSnap.data().storagePaths exists for docx/preview/thumb to avoid pointing “master” at an incomplete asset.

3) One-time migration (if you already wrote thumbs under /previews/)

Use Admin SDK (Node) to copy files server-side and update Firestore paths.

Copy Storage files
// node: migrate-thumbs.js (run once)
import { initializeApp, cert } from "firebase-admin/app";
import { getStorage } from "firebase-admin/storage";
import { getFirestore } from "firebase-admin/firestore";

// initializeApp({ credential: cert("./serviceAccountKey.json"), storageBucket: "<your-bucket>" });
initializeApp(); // if running where env is already authorized

const db = getFirestore();
const bucket = getStorage().bucket();

async function moveThumb(slug, version = "v1") {
  const oldPath = `templates/business-plan/previews/${slug}/${version}-thumb.webp`;
  const newPath = `templates/business-plan/thumbs/${slug}/${version}-thumb.webp`;

  const [exists] = await bucket.file(oldPath).exists();
  if (!exists) { console.log(`Skip (no old thumb): ${oldPath}`); return; }

  await bucket.file(oldPath).copy(bucket.file(newPath));
  console.log(`Copied: ${oldPath} -> ${newPath}`);

  // Optional: delete old
  await bucket.file(oldPath).delete().catch(() => {});
}

async function updateFirestore(slug, version = "v1") {
  const slugRef = db.doc(`templates/business-plan/types/${slug}`);
  await slugRef.update({
    "storagePaths.thumb": `templates/business-plan/thumbs/${slug}/${version}-thumb.webp`,
    updatedAt: new Date()
  });

  const verRef = db.doc(`templates/business-plan/types/${slug}/versions/${version}`);
  await verRef.set({
    storagePaths: {
      thumb: `templates/business-plan/thumbs/${slug}/${version}-thumb.webp`
    }
  }, { merge: true });

  console.log(`Firestore updated for ${slug}/${version}`);
}

async function run() {
  const q = await db.collection("templates/business-plan/types").get();
  for (const docSnap of q.docs) {
    const slug = docSnap.id;
    const version = docSnap.get("currentVersion") || "v1";
    await moveThumb(slug, version);
    await updateFirestore(slug, version);
  }
  console.log("Thumb migration complete.");
}

run().catch(console.error);

4) Admin UI tweaks (tiny)

Uploader output paths

Preview → templates/business-plan/previews/<slug>/<version>-preview.webp

Thumb → templates/business-plan/thumbs/<slug>/<version>-thumb.webp

Promotion button

Calls promoteVersionToMaster({ slug, version }) (the transaction above).

Disable while running; show toast on success.

5) Quick re-test checklist

Upload DOCX → Firestore doc created, currentVersion = v1.

Generated images land at:

.../previews/<slug>/v1-preview.webp

.../thumbs/<slug>/v1-thumb.webp ✅

Run Promote on v1 → transaction logs OK.

/api/bp-templates-firebase/list returns isMaster: true, thumb path under /thumbs/.

Public GET on both preview and thumb returns 200 OK.