diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6306f93..1c53bda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
+## 0.44.0 — 2026-07-17 16:15
+
+### Added
+- New "Team" billing tier, above Pro — higher AI-call, recipe, and storage limits, editable from Admin > Tiers like the existing tiers. (Moderator/admin remain separate account roles, unrelated to billing tier — unchanged by this.)
+
## 0.43.0 — 2026-07-17 15:45
### Added
diff --git a/apps/web/app/admin/storage/page.tsx b/apps/web/app/admin/storage/page.tsx
index fbdbe81..0f834ff 100644
--- a/apps/web/app/admin/storage/page.tsx
+++ b/apps/web/app/admin/storage/page.tsx
@@ -41,6 +41,7 @@ export default async function AdminStoragePage() {
const freePhotos = Number(photosByTier.find((r) => r.tier === "free")?.photoCount ?? 0);
const proPhotos = Number(photosByTier.find((r) => r.tier === "pro")?.photoCount ?? 0);
+ const teamPhotos = Number(photosByTier.find((r) => r.tier === "team")?.photoCount ?? 0);
return (
@@ -51,7 +52,7 @@ export default async function AdminStoragePage() {
-
+
Total Photos
@@ -82,6 +83,16 @@ export default async function AdminStoragePage() {
{proPhotos.toLocaleString()}
+
+
+
+ Team Tier Photos
+
+
+
+ {teamPhotos.toLocaleString()}
+
+
diff --git a/apps/web/app/admin/users/[id]/page.tsx b/apps/web/app/admin/users/[id]/page.tsx
index 340b7cc..ecbdaa6 100644
--- a/apps/web/app/admin/users/[id]/page.tsx
+++ b/apps/web/app/admin/users/[id]/page.tsx
@@ -25,6 +25,7 @@ const ROLE_COLORS = {
const TIER_COLORS = {
free: "secondary",
pro: "default",
+ team: "default",
} as const;
export default async function AdminUserDetailPage({ params }: PageProps) {
diff --git a/apps/web/app/admin/users/page.tsx b/apps/web/app/admin/users/page.tsx
index bf93b72..4d6c7bd 100644
--- a/apps/web/app/admin/users/page.tsx
+++ b/apps/web/app/admin/users/page.tsx
@@ -24,6 +24,7 @@ const ROLE_COLORS = {
const TIER_COLORS = {
free: "secondary",
pro: "default",
+ team: "default",
} as const;
export default async function AdminUsersPage({ searchParams }: PageProps) {
diff --git a/apps/web/app/api/v1/admin/invites/route.ts b/apps/web/app/api/v1/admin/invites/route.ts
index 90c40bd..2dfa1e5 100644
--- a/apps/web/app/api/v1/admin/invites/route.ts
+++ b/apps/web/app/api/v1/admin/invites/route.ts
@@ -5,7 +5,7 @@ import { createInvite, listInvites } from "@/lib/invites";
import { randomUUID } from "crypto";
const VALID_ROLES = ["user", "moderator", "admin"] as const;
-const VALID_TIERS = ["free", "pro"] as const;
+const VALID_TIERS = ["free", "pro", "team"] as const;
export async function GET() {
const { response } = await requireAdmin();
diff --git a/apps/web/app/api/v1/admin/tiers/[tier]/route.ts b/apps/web/app/api/v1/admin/tiers/[tier]/route.ts
index 0b24034..1477b34 100644
--- a/apps/web/app/api/v1/admin/tiers/[tier]/route.ts
+++ b/apps/web/app/api/v1/admin/tiers/[tier]/route.ts
@@ -16,7 +16,7 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) {
if (response) return response;
const { tier } = await params;
- if (tier !== "free" && tier !== "pro") {
+ if (tier !== "free" && tier !== "pro" && tier !== "team") {
return NextResponse.json({ error: "Invalid tier" }, { status: 400 });
}
diff --git a/apps/web/app/api/v1/admin/users/[id]/route.ts b/apps/web/app/api/v1/admin/users/[id]/route.ts
index b0be0df..d779963 100644
--- a/apps/web/app/api/v1/admin/users/[id]/route.ts
+++ b/apps/web/app/api/v1/admin/users/[id]/route.ts
@@ -16,7 +16,7 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) {
const { role, tier } = body;
const validRoles = ["user", "moderator", "admin"] as const;
- const validTiers = ["free", "pro"] as const;
+ const validTiers = ["free", "pro", "team"] as const;
if (role !== undefined && !validRoles.includes(role as typeof validRoles[number])) {
return NextResponse.json({ error: "Invalid role" }, { status: 400 });
@@ -25,11 +25,11 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) {
return NextResponse.json({ error: "Invalid tier" }, { status: 400 });
}
- const updateData: Partial<{ role: "user" | "moderator" | "admin"; tier: "free" | "pro"; updatedAt: Date }> = {
+ const updateData: Partial<{ role: "user" | "moderator" | "admin"; tier: "free" | "pro" | "team"; updatedAt: Date }> = {
updatedAt: new Date(),
};
if (role) updateData.role = role as "user" | "moderator" | "admin";
- if (tier) updateData.tier = tier as "free" | "pro";
+ if (tier) updateData.tier = tier as "free" | "pro" | "team";
const [updated] = await db
.update(users)
diff --git a/apps/web/app/api/v1/admin/users/route.ts b/apps/web/app/api/v1/admin/users/route.ts
index 4e054d7..075d6f4 100644
--- a/apps/web/app/api/v1/admin/users/route.ts
+++ b/apps/web/app/api/v1/admin/users/route.ts
@@ -7,7 +7,7 @@ import { randomBytes, randomUUID } from "crypto";
import { APIError } from "better-auth";
const VALID_ROLES = ["user", "moderator", "admin"] as const;
-const VALID_TIERS = ["free", "pro"] as const;
+const VALID_TIERS = ["free", "pro", "team"] as const;
export async function POST(req: NextRequest) {
const { session, response } = await requireAdmin();
diff --git a/apps/web/app/api/v1/ai/adapt/[id]/route.ts b/apps/web/app/api/v1/ai/adapt/[id]/route.ts
index a1acf96..f357272 100644
--- a/apps/web/app/api/v1/ai/adapt/[id]/route.ts
+++ b/apps/web/app/api/v1/ai/adapt/[id]/route.ts
@@ -52,7 +52,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(userId, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
adaptRecipe(
{
title: recipe.title,
@@ -80,7 +80,7 @@ export async function POST(req: NextRequest, { params }: Params) {
}
try {
- await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro", "recipe");
+ await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "team", "recipe");
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "Recipe limit reached for your tier" }, { status: 403 });
diff --git a/apps/web/app/api/v1/ai/batch-cook/generate/route.ts b/apps/web/app/api/v1/ai/batch-cook/generate/route.ts
index 1ba0b4f..4c41510 100644
--- a/apps/web/app/api/v1/ai/batch-cook/generate/route.ts
+++ b/apps/web/app/api/v1/ai/batch-cook/generate/route.ts
@@ -43,7 +43,7 @@ export async function POST(req: NextRequest) {
if (!configResult.ok) return configResult.response;
const config = configResult.data;
- const result = await withAiQuota(userId, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
generateBatchCook(
{
dinners: parsed.data.dinners,
diff --git a/apps/web/app/api/v1/ai/cooking-chat/route.ts b/apps/web/app/api/v1/ai/cooking-chat/route.ts
index f09067b..d1aabb8 100644
--- a/apps/web/app/api/v1/ai/cooking-chat/route.ts
+++ b/apps/web/app/api/v1/ai/cooking-chat/route.ts
@@ -40,7 +40,7 @@ export async function POST(req: NextRequest) {
const locale = (session!.user as { locale?: string }).locale ?? "en";
const lang = LANG[locale] ?? "English";
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
generateText({
model,
system: `You are Epicure, a helpful culinary assistant answering general cooking questions — not tied to any specific recipe (techniques, substitutions, timing, equipment, food safety, etc). If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question has nothing to do with cooking or food, politely redirect. Keep answers under 200 words. Respond in ${lang}.${bioContext}`,
diff --git a/apps/web/app/api/v1/ai/drinks/[id]/route.ts b/apps/web/app/api/v1/ai/drinks/[id]/route.ts
index cda4b78..c9333ab 100644
--- a/apps/web/app/api/v1/ai/drinks/[id]/route.ts
+++ b/apps/web/app/api/v1/ai/drinks/[id]/route.ts
@@ -43,7 +43,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
suggestDrinks(
{
title: recipe.title,
diff --git a/apps/web/app/api/v1/ai/generate-from-idea/route.ts b/apps/web/app/api/v1/ai/generate-from-idea/route.ts
index e4940bb..b3a47da 100644
--- a/apps/web/app/api/v1/ai/generate-from-idea/route.ts
+++ b/apps/web/app/api/v1/ai/generate-from-idea/route.ts
@@ -39,7 +39,7 @@ export async function POST(req: NextRequest) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
generateRecipe(parsed.data.title, {
...aiConfig,
userContext: privateBio ?? undefined,
diff --git a/apps/web/app/api/v1/ai/generate/route.ts b/apps/web/app/api/v1/ai/generate/route.ts
index 1ff9f30..b5611dc 100644
--- a/apps/web/app/api/v1/ai/generate/route.ts
+++ b/apps/web/app/api/v1/ai/generate/route.ts
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
generateRecipe(parsed.data.prompt, {
...aiConfig,
language: parsed.data.language,
diff --git a/apps/web/app/api/v1/ai/import-photo/route.ts b/apps/web/app/api/v1/ai/import-photo/route.ts
index 1591137..5316388 100644
--- a/apps/web/app/api/v1/ai/import-photo/route.ts
+++ b/apps/web/app/api/v1/ai/import-photo/route.ts
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
if (!textConfigResult.ok) return textConfigResult.response;
const textConfig = textConfigResult.data;
- const result = await withAiQuota(userId, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
importFromPhoto(parsed.data.imageBase64, parsed.data.mimeType, visionConfig, textConfig, locale),
{ skipQuota: visionConfig.isByok && textConfig.isByok }
);
diff --git a/apps/web/app/api/v1/ai/import-url/route.ts b/apps/web/app/api/v1/ai/import-url/route.ts
index f2f14bb..16d414c 100644
--- a/apps/web/app/api/v1/ai/import-url/route.ts
+++ b/apps/web/app/api/v1/ai/import-url/route.ts
@@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
importFromUrl(parsed.data.url, aiConfig), { skipQuota: aiConfig.isByok }
);
if (!result.ok) return result.response;
diff --git a/apps/web/app/api/v1/ai/meal-plan/generate/route.ts b/apps/web/app/api/v1/ai/meal-plan/generate/route.ts
index 4577b61..5adbc3f 100644
--- a/apps/web/app/api/v1/ai/meal-plan/generate/route.ts
+++ b/apps/web/app/api/v1/ai/meal-plan/generate/route.ts
@@ -75,7 +75,7 @@ export async function POST(req: NextRequest) {
}
}
- const result = await withAiQuota(userId, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
generateMealPlan(
{
dietaryPrefs: parsed.data.dietaryPrefs,
@@ -99,7 +99,7 @@ export async function POST(req: NextRequest) {
let chargedRecipes = 0;
try {
for (let i = 0; i < plan.entries.length; i++) {
- await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro", "recipe");
+ await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "team", "recipe");
chargedRecipes++;
}
} catch (err) {
diff --git a/apps/web/app/api/v1/ai/pairings/[id]/route.ts b/apps/web/app/api/v1/ai/pairings/[id]/route.ts
index 1d9e368..c35c600 100644
--- a/apps/web/app/api/v1/ai/pairings/[id]/route.ts
+++ b/apps/web/app/api/v1/ai/pairings/[id]/route.ts
@@ -44,7 +44,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
suggestPairings(
{
title: recipe.title,
diff --git a/apps/web/app/api/v1/ai/recipe-chat/route.ts b/apps/web/app/api/v1/ai/recipe-chat/route.ts
index 810b1c4..397c354 100644
--- a/apps/web/app/api/v1/ai/recipe-chat/route.ts
+++ b/apps/web/app/api/v1/ai/recipe-chat/route.ts
@@ -74,7 +74,7 @@ ${stepList || "None listed"}
const locale = (session!.user as { locale?: string }).locale ?? "en";
const lang = LANG[locale] ?? "English";
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
generateText({
model,
system: `You are Epicure, a helpful culinary assistant. Answer questions about the following recipe concisely and accurately. If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question is not related to the recipe or cooking, politely redirect. Keep answers under 200 words. Respond in ${lang}.
diff --git a/apps/web/app/api/v1/ai/recipe-ideas/route.ts b/apps/web/app/api/v1/ai/recipe-ideas/route.ts
index c770e73..1c1a9ef 100644
--- a/apps/web/app/api/v1/ai/recipe-ideas/route.ts
+++ b/apps/web/app/api/v1/ai/recipe-ideas/route.ts
@@ -57,7 +57,7 @@ export async function POST(req: NextRequest) {
? `${userContext}Generate 6 diverse recipe ideas based on: "${parsed.data.prompt}". Include a mix of difficulty levels.`
: `${userContext}Generate 6 diverse, creative recipe ideas. Include different cuisines, difficulty levels, and meal types.`;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
generateObject({
model,
schema: IdeasSchema,
diff --git a/apps/web/app/api/v1/ai/regenerate/route.ts b/apps/web/app/api/v1/ai/regenerate/route.ts
index 5b5c307..cdddcbb 100644
--- a/apps/web/app/api/v1/ai/regenerate/route.ts
+++ b/apps/web/app/api/v1/ai/regenerate/route.ts
@@ -47,7 +47,7 @@ export async function POST(req: NextRequest) {
const { instruction, language, ...current } = parsed.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
regenerateRecipe(current, instruction, { ...aiConfig, language }), { skipQuota: aiConfig.isByok }
);
if (!result.ok) return result.response;
diff --git a/apps/web/app/api/v1/ai/scale/route.ts b/apps/web/app/api/v1/ai/scale/route.ts
index 23101e3..118dd2d 100644
--- a/apps/web/app/api/v1/ai/scale/route.ts
+++ b/apps/web/app/api/v1/ai/scale/route.ts
@@ -44,7 +44,7 @@ export async function POST(req: NextRequest) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
scaleRecipe(
{
title: recipe.title,
diff --git a/apps/web/app/api/v1/ai/substitute/route.ts b/apps/web/app/api/v1/ai/substitute/route.ts
index d8af73a..6f45c62 100644
--- a/apps/web/app/api/v1/ai/substitute/route.ts
+++ b/apps/web/app/api/v1/ai/substitute/route.ts
@@ -40,7 +40,7 @@ export async function POST(req: NextRequest) {
const locale = (session!.user as { locale?: string }).locale ?? "en";
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
substituteIngredient(parsed.data.ingredient, context, aiConfig, locale),
{ skipQuota: aiConfig.isByok }
);
diff --git a/apps/web/app/api/v1/ai/translate/[id]/route.ts b/apps/web/app/api/v1/ai/translate/[id]/route.ts
index a3dc367..dc7ca43 100644
--- a/apps/web/app/api/v1/ai/translate/[id]/route.ts
+++ b/apps/web/app/api/v1/ai/translate/[id]/route.ts
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
translateRecipe(
{
title: recipe.title,
diff --git a/apps/web/app/api/v1/ai/variations/[id]/route.ts b/apps/web/app/api/v1/ai/variations/[id]/route.ts
index d7f990e..a1c131a 100644
--- a/apps/web/app/api/v1/ai/variations/[id]/route.ts
+++ b/apps/web/app/api/v1/ai/variations/[id]/route.ts
@@ -47,7 +47,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
suggestVariations(
{
title: recipe.title,
diff --git a/apps/web/app/api/v1/pantry/scan/photo/route.ts b/apps/web/app/api/v1/pantry/scan/photo/route.ts
index 626e813..2da15ce 100644
--- a/apps/web/app/api/v1/pantry/scan/photo/route.ts
+++ b/apps/web/app/api/v1/pantry/scan/photo/route.ts
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
else if (aiConfig.provider === "anthropic") aiConfig.model = "claude-sonnet-4-6";
}
- const result = await withAiQuota(userId, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
scanPantryPhoto(parsed.data.imageBase64, parsed.data.mimeType, aiConfig)
);
if (!result.ok) return result.response;
diff --git a/apps/web/app/api/v1/recipes/[id]/fork/route.ts b/apps/web/app/api/v1/recipes/[id]/fork/route.ts
index f80e2b5..85e87dc 100644
--- a/apps/web/app/api/v1/recipes/[id]/fork/route.ts
+++ b/apps/web/app/api/v1/recipes/[id]/fork/route.ts
@@ -24,7 +24,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!source) return NextResponse.json({ error: "Not found" }, { status: 404 });
try {
- await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "recipe");
+ await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro" | "team", "recipe");
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "Recipe limit reached for your tier" }, { status: 403 });
diff --git a/apps/web/app/api/v1/recipes/[id]/nutrition/route.ts b/apps/web/app/api/v1/recipes/[id]/nutrition/route.ts
index 9a7dddb..7be89f1 100644
--- a/apps/web/app/api/v1/recipes/[id]/nutrition/route.ts
+++ b/apps/web/app/api/v1/recipes/[id]/nutrition/route.ts
@@ -46,7 +46,7 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!recipe) return NextResponse.json({ error: "Not found" }, { status: 404 });
- const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro", () =>
+ const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
estimateNutrition({
title: recipe.title,
baseServings: recipe.baseServings,
diff --git a/apps/web/app/api/v1/recipes/route.ts b/apps/web/app/api/v1/recipes/route.ts
index 29e06b1..733f3d9 100644
--- a/apps/web/app/api/v1/recipes/route.ts
+++ b/apps/web/app/api/v1/recipes/route.ts
@@ -117,7 +117,7 @@ export async function POST(req: NextRequest) {
}
try {
- await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "recipe");
+ await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro" | "team", "recipe");
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "Recipe limit reached for your tier" }, { status: 403 });
diff --git a/apps/web/app/api/v1/upload/avatar-presign/route.ts b/apps/web/app/api/v1/upload/avatar-presign/route.ts
index da48afd..9fbae5c 100644
--- a/apps/web/app/api/v1/upload/avatar-presign/route.ts
+++ b/apps/web/app/api/v1/upload/avatar-presign/route.ts
@@ -29,7 +29,7 @@ export async function POST(req: NextRequest) {
try {
const sizeMb = Math.ceil(fileSize / (1024 * 1024));
- await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "storage", sizeMb);
+ await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro" | "team", "storage", sizeMb);
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "Storage limit reached for your tier" }, { status: 403 });
diff --git a/apps/web/app/api/v1/upload/presign/route.ts b/apps/web/app/api/v1/upload/presign/route.ts
index e321423..41b6eb0 100644
--- a/apps/web/app/api/v1/upload/presign/route.ts
+++ b/apps/web/app/api/v1/upload/presign/route.ts
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
try {
const sizeMb = Math.ceil(fileSize / (1024 * 1024));
- await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "storage", sizeMb);
+ await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro" | "team", "storage", sizeMb);
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "Storage limit reached for your tier" }, { status: 403 });
diff --git a/apps/web/components/admin/create-user-dialog.tsx b/apps/web/components/admin/create-user-dialog.tsx
index 7eb2dd1..a2e7a41 100644
--- a/apps/web/components/admin/create-user-dialog.tsx
+++ b/apps/web/components/admin/create-user-dialog.tsx
@@ -16,7 +16,7 @@ export function CreateUserDialog() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [role, setRole] = useState<"user" | "moderator" | "admin">("user");
- const [tier, setTier] = useState<"free" | "pro">("free");
+ const [tier, setTier] = useState<"free" | "pro" | "team">("free");
const [saving, setSaving] = useState(false);
async function handleCreate() {
@@ -85,6 +85,7 @@ export function CreateUserDialog() {
Free
Pro
+ Team
diff --git a/apps/web/components/admin/invites-manager.tsx b/apps/web/components/admin/invites-manager.tsx
index decb905..f56bf58 100644
--- a/apps/web/components/admin/invites-manager.tsx
+++ b/apps/web/components/admin/invites-manager.tsx
@@ -24,7 +24,7 @@ type Invite = {
token: string;
email: string | null;
role: "user" | "moderator" | "admin";
- tier: "free" | "pro";
+ tier: "free" | "pro" | "team";
createdAt: string;
expiresAt: string | null;
};
@@ -33,7 +33,7 @@ export function InvitesManager({ invites, appUrl }: { invites: Invite[]; appUrl:
const router = useRouter();
const [email, setEmail] = useState("");
const [role, setRole] = useState<"user" | "moderator" | "admin">("user");
- const [tier, setTier] = useState<"free" | "pro">("free");
+ const [tier, setTier] = useState<"free" | "pro" | "team">("free");
const [creating, setCreating] = useState(false);
const [revokeId, setRevokeId] = useState
(null);
@@ -106,6 +106,7 @@ export function InvitesManager({ invites, appUrl }: { invites: Invite[]; appUrl:
Free
Pro
+ Team
diff --git a/apps/web/components/admin/user-editor.tsx b/apps/web/components/admin/user-editor.tsx
index 3561090..0c318a8 100644
--- a/apps/web/components/admin/user-editor.tsx
+++ b/apps/web/components/admin/user-editor.tsx
@@ -15,12 +15,12 @@ import { Label } from "@/components/ui/label";
interface UserEditorProps {
userId: string;
currentRole: "user" | "moderator" | "admin";
- currentTier: "free" | "pro";
+ currentTier: "free" | "pro" | "team";
}
export function UserEditor({ userId, currentRole, currentTier }: UserEditorProps) {
const [role, setRole] = useState<"user" | "moderator" | "admin">(currentRole);
- const [tier, setTier] = useState<"free" | "pro">(currentTier);
+ const [tier, setTier] = useState<"free" | "pro" | "team">(currentTier);
const [saving, setSaving] = useState(false);
async function handleSave() {
@@ -68,6 +68,7 @@ export function UserEditor({ userId, currentRole, currentTier }: UserEditorProps