Files
webstudio/apps/builder/app/shared/db/user-plan-features.server.ts
Oleg Isonen b8d07af7e6 feat: gate workspace features behind maxWorkspaces plan limit
- Replace workspaces feature flag with maxWorkspaces-based gating
- Default maxWorkspaces to 0 (controlled via MAX_WORKSPACES env var)
- Skip workspace queries in dashboard loader when maxWorkspaces is 0
- Extract pure functions (resolveCurrentWorkspace, isWorkspaceRelationPermitted, getView) with tests
- Remove workspaces feature flag from all files
2026-03-16 15:58:16 +00:00

142 lines
4.4 KiB
TypeScript

import type { AppContext } from "@webstudio-is/trpc-interface/index.server";
import type { UserPlanFeatures } from "@webstudio-is/trpc-interface/user-plan-features";
import env from "~/env/env.server";
type UserPlanInfo = {
userPlanFeatures: UserPlanFeatures;
purchases: AppContext["purchases"];
};
export const getUserPlanInfo = async (
userId: string,
postgrest: AppContext["postgrest"]
): Promise<UserPlanInfo> => {
const userProductsResult = await postgrest.client
.from("UserProduct")
.select("customerId, subscriptionId, productId")
.eq("userId", userId);
if (userProductsResult.error) {
console.error(userProductsResult.error);
throw new Error("Failed to fetch user products");
}
const userProducts = userProductsResult.data;
const productsResult = await postgrest.client
.from("Product")
.select("id, name, meta")
.in(
"id",
userProducts.map(({ productId }) => productId ?? "")
);
if (productsResult.error) {
console.error(productsResult.error);
throw new Error("Failed to fetch products");
}
const products = productsResult.data;
// Create a map of productId -> product name for quick lookup
const productIdToName = new Map<string, string>();
for (const product of products) {
productIdToName.set(product.id, product.name);
}
// Build purchases array - includes all products (subscriptions and LTDs)
// subscriptionId only set for recurring subscriptions (manageable in Stripe)
const purchases: Array<{
planName: string;
subscriptionId?: string;
}> = [];
for (const userProduct of userProducts) {
if (userProduct.productId) {
const planName = productIdToName.get(userProduct.productId) ?? "Pro";
purchases.push({
planName,
subscriptionId: userProduct.subscriptionId ?? undefined,
});
}
}
if (userProducts.length > 0) {
const productMetas = products.map((product) => {
return {
canDownloadAssets: true,
canRestoreBackups: true,
allowAdditionalPermissions: true,
allowDynamicData: true,
allowContentMode: true,
allowStagingPublish: true,
maxContactEmails: 5,
maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,
maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,
maxWorkspaces: env.MAX_WORKSPACES,
...(product.meta as Partial<UserPlanFeatures>),
};
});
return {
userPlanFeatures: {
canDownloadAssets: productMetas.some((item) => item.canDownloadAssets),
canRestoreBackups: productMetas.some((item) => item.canRestoreBackups),
allowAdditionalPermissions: productMetas.some(
(item) => item.allowAdditionalPermissions
),
allowDynamicData: productMetas.some((item) => item.allowDynamicData),
allowContentMode: productMetas.some((item) => item.allowContentMode),
allowStagingPublish: productMetas.some(
(item) => item.allowStagingPublish
),
maxContactEmails: Math.max(
...productMetas.map((item) => item.maxContactEmails)
),
maxDomainsAllowedPerUser: Math.max(
...productMetas.map((item) => item.maxDomainsAllowedPerUser)
),
maxPublishesAllowedPerUser: Math.max(
...productMetas.map((item) => item.maxPublishesAllowedPerUser)
),
maxWorkspaces: Math.max(
...productMetas.map((item) => item.maxWorkspaces)
),
},
purchases,
};
}
if (env.USER_PLAN === "pro") {
return {
userPlanFeatures: {
canDownloadAssets: true,
canRestoreBackups: true,
allowAdditionalPermissions: true,
allowDynamicData: true,
allowContentMode: true,
allowStagingPublish: true,
maxContactEmails: 5,
maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,
maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,
maxWorkspaces: env.MAX_WORKSPACES,
},
purchases: [{ planName: "env.USER_PLAN Pro" }],
};
}
return {
userPlanFeatures: {
canDownloadAssets: false,
canRestoreBackups: false,
allowAdditionalPermissions: false,
allowDynamicData: false,
allowContentMode: false,
allowStagingPublish: false,
maxContactEmails: 0,
maxDomainsAllowedPerUser: 0,
maxPublishesAllowedPerUser: 10,
maxWorkspaces: env.MAX_WORKSPACES,
},
purchases: [],
};
};