Here’s a clean, drop-in quota + license layer that:

tracks monthly download quotas per subscription (5/10/25),

decrements on first successful original download,

grants permanent licenses for one-offs (no quota impact),

blocks original when quota is 0 (falls back to watermarked preview).

As you like: full-file replacements + two new files. No changes to your watermark/storage code.

1) New file — config/priceQuotas.js
// config/priceQuotas.js
// Map Stripe price lookup keys -> monthly included download count.
module.exports = {
  ibrandbiz_pro_monthly: 5, // IBrandBiz $19 Pro bundle includes 5/mo
  stock_starter_mo: 5,      // Stock Starter
  stock_pro_mo: 10,         // Stock Pro
  stock_agency_mo: 25,      // Stock Agency (adjust to 40 later if you want)
};

2) New file — services/entitlements.js
// services/entitlements.js
// Minimal JSON-backed entitlements store (licenses + monthly quotas)

const fs = require("fs");
const path = require("path");
const quotas = require("../config/priceQuotas");

const DB_DIR = path.join(process.cwd(), "data");
const DB_FILE = path.join(DB_DIR, "entitlements-db.json");

function ensureDB() {
  if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
  if (!fs.existsSync(DB_FILE)) {
    fs.writeFileSync(DB_FILE, JSON.stringify({ users: {} }, null, 2));
  }
}

function load() {
  ensureDB();
  return JSON.parse(fs.readFileSync(DB_FILE, "utf8"));
}

function save(db) {
  fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
}

function getUser(db, userId) {
  if (!db.users[userId]) db.users[userId] = { licenses: {}, quotas: [] };
  return db.users[userId];
}

// ---- Licenses --------------------------------------------------------------

function grantLicenses(userId, assetIds = []) {
  if (!userId || !assetIds.length) return;
  const db = load();
  const user = getUser(db, userId);
  for (const id of assetIds) user.licenses[id] = true;
  save(db);
}

function hasLicense(userId, assetId) {
  if (!userId) return false;
  const db = load();
  const user = getUser(db, userId);
  return !!user.licenses[assetId];
}

// ---- Quotas ----------------------------------------------------------------

/**
 * Upsert quotas for a Stripe subscription object.
 * subscription.items.data[i].price.lookup_key must match keys in config/priceQuotas.js
 */
function upsertQuotasFromStripeSubscription(userId, subscription) {
  if (!userId || !subscription) return;
  const db = load();
  const user = getUser(db, userId);

  const periodStart = Number(subscription.current_period_start) * 1000;
  const periodEnd = Number(subscription.current_period_end) * 1000;

  // Build desired windows for this subscription
  const desired = [];
  for (const item of subscription.items?.data || []) {
    const lookup = item.price?.lookup_key;
    const include = quotas[lookup] || 0;
    if (!lookup || include <= 0) continue;

    desired.push({
      subscriptionId: subscription.id,
      lookupKey: lookup,
      periodStart,
      periodEnd,
      included: include,
    });
  }

  // Upsert logic: find matching (subId + lookupKey + periodStart). If new period, reset remaining.
  for (const d of desired) {
    const idx = user.quotas.findIndex(
      (q) =>
        q.subscriptionId === d.subscriptionId &&
        q.lookupKey === d.lookupKey &&
        q.periodStart === d.periodStart
    );
    if (idx === -1) {
      user.quotas.push({
        ...d,
        remaining: d.included,
        updatedAt: Date.now(),
      });
    } else {
      // keep remaining if we’re in same period; update included/period bounds
      const existing = user.quotas[idx];
      user.quotas[idx] = {
        ...existing,
        included: d.included,
        periodStart: d.periodStart,
        periodEnd: d.periodEnd,
        updatedAt: Date.now(),
      };
    }
  }

  // Garbage-collect old periods for this subscription (optional)
  user.quotas = user.quotas.filter((q) => {
    // keep entries not tied to this sub, or current/most recent periods
    if (q.subscriptionId !== subscription.id) return true;
    // keep any period that ends within last ~90 days
    return q.periodEnd > Date.now() - 90 * 24 * 3600 * 1000;
  });

  save(db);
}

/**
 * Sum remaining quota across all active windows at "now".
 */
function getTotalRemaining(userId, now = Date.now()) {
  if (!userId) return 0;
  const db = load();
  const user = getUser(db, userId);
  return user.quotas
    .filter((q) => q.periodStart <= now && now < q.periodEnd)
    .reduce((sum, q) => sum + Math.max(0, q.remaining || 0), 0);
}

/**
 * Decrement one unit from the earliest-expiring active windows.
 * Returns true if decremented; false if insufficient.
 */
function decrementOne(userId, now = Date.now()) {
  if (!userId) return false;
  const db = load();
  const user = getUser(db, userId);
  const active = user.quotas
    .filter((q) => q.periodStart <= now && now < q.periodEnd && (q.remaining || 0) > 0)
    .sort((a, b) => a.periodEnd - b.periodEnd); // burn the soonest-to-expire first

  if (!active.length) return false;

  active[0].remaining = Math.max(0, (active[0].remaining || 0) - 1);
  active[0].updatedAt = now;
  save(db);
  return true;
}

module.exports = {
  grantLicenses,
  hasLicense,
  upsertQuotasFromStripeSubscription,
  getTotalRemaining,
  decrementOne,
};
