diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8f23d90..4dced14 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -441,6 +441,9 @@ importers:
'@auth/express':
specifier: ^0.5.5
version: 0.5.6(express@4.18.2)(nodemailer@6.9.8)
+ '@lemonsqueezy/lemonsqueezy.js':
+ specifier: ^3.3.1
+ version: 3.3.1
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
@@ -2334,6 +2337,14 @@ packages:
'@leichtgewicht/ip-codec@2.0.5':
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
+ '@lemonsqueezy/lemonsqueezy.js@3.3.1':
+ resolution: {integrity: sha512-gM/FdNsK3BlrD6JRrhmiyqBXQsCpzSUdKSoZwJMQfXqfqcK321og+uMssc6HYcygUMrGvPnNJyJ1RqZPFDrgtg==}
+ engines: {node: '>=20'}
+
+ '@ljharb/resumer@0.0.1':
+ resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==}
+ engines: {node: '>= 0.4'}
+
'@ljharb/through@2.3.11':
resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==}
engines: {node: '>= 0.4'}
@@ -15285,6 +15296,12 @@ snapshots:
'@leichtgewicht/ip-codec@2.0.5': {}
+ '@lemonsqueezy/lemonsqueezy.js@3.3.1': {}
+
+ '@ljharb/resumer@0.0.1':
+ dependencies:
+ '@ljharb/through': 2.3.11
+
'@ljharb/through@2.3.11':
dependencies:
call-bind: 1.0.7
diff --git a/src/client/routes/playground.tsx b/src/client/routes/playground.tsx
index e867a4f..ac238d0 100644
--- a/src/client/routes/playground.tsx
+++ b/src/client/routes/playground.tsx
@@ -8,6 +8,12 @@ import { useState } from 'react';
import { EditableText } from '@/components/EditableText';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { WebhookPlayground } from '@/components/WebhookPlayground';
+import React from 'react';
+import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
+import { Button } from '@/components/ui/button';
+import { useEvent } from '@/hooks/useEvent';
+import { useCurrentWorkspaceId } from '@/store/user';
+import { Separator } from '@/components/ui/separator';
export const Route = createFileRoute('/playground')({
beforeLoad: () => {
@@ -48,18 +54,22 @@ function PageComponent() {
return (
-
+
- Current
- History
+ Billing
+ Webhook
+ Misc
-
+
+
+
+
-
+
);
}
+
+export const BillingPlayground: React.FC = React.memo(() => {
+ const checkoutMutation = trpc.billing.checkout.useMutation({
+ onSuccess: defaultSuccessHandler,
+ onError: defaultErrorHandler,
+ });
+ const changePlanMutation = trpc.billing.changePlan.useMutation({
+ onSuccess: defaultSuccessHandler,
+ onError: defaultErrorHandler,
+ });
+ const cancelSubscriptionMutation =
+ trpc.billing.cancelSubscription.useMutation({
+ onSuccess: defaultSuccessHandler,
+ onError: defaultErrorHandler,
+ });
+ const workspaceId = useCurrentWorkspaceId();
+ const { data, refetch, isInitialLoading, isLoading } =
+ trpc.billing.currentSubscription.useQuery({
+ workspaceId,
+ });
+
+ const handleCheckoutSubscribe = useEvent(async (tier: 'pro' | 'team') => {
+ const { url } = await checkoutMutation.mutateAsync({
+ workspaceId,
+ tier,
+ redirectUrl: location.href,
+ });
+
+ location.href = url;
+ });
+
+ const handleChangeSubscribe = useEvent(
+ async (tier: 'free' | 'pro' | 'team') => {
+ await changePlanMutation.mutateAsync({
+ workspaceId,
+ tier,
+ });
+
+ refetch();
+ }
+ );
+
+ const plan = data ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ );
+
+ return (
+
+
+
Current: {JSON.stringify(data)}
+
+
+
+
+
+
+ {isInitialLoading === false && plan}
+
+ );
+});
+BillingPlayground.displayName = 'BillingPlayground';
diff --git a/src/server/app.ts b/src/server/app.ts
index acc1f77..b9036fb 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -23,6 +23,7 @@ import { monitorPageManager } from './model/monitor/page/manager.js';
import { ExpressAuth } from '@auth/express';
import { authConfig } from './model/auth.js';
import { prometheusApiVersion } from './middleware/prometheus/index.js';
+import { billingRouter } from './router/billing.js';
const app = express();
@@ -32,6 +33,9 @@ app.use(compression());
app.use(
express.json({
limit: '10mb',
+ verify: (req, res, buf) => {
+ (req as any).rawBody = buf;
+ },
})
);
app.use(passport.initialize());
@@ -51,6 +55,7 @@ app.use(
app.use('/health', healthRouter);
app.use('/api/auth/*', ExpressAuth(authConfig));
app.use('/api/website', websiteRouter);
+app.use('/api/billing', billingRouter);
app.use('/monitor', monitorRouter);
app.use('/telemetry', telemetryRouter);
app.use('/serverStatus', serverStatusRouter);
diff --git a/src/server/model/billing.ts b/src/server/model/billing.ts
new file mode 100644
index 0000000..31c090e
--- /dev/null
+++ b/src/server/model/billing.ts
@@ -0,0 +1,153 @@
+import {
+ lemonSqueezySetup,
+ createCheckout,
+ updateSubscription,
+ cancelSubscription as lsCancelSubscription,
+} from '@lemonsqueezy/lemonsqueezy.js';
+import { env } from '../utils/env.js';
+import { prisma } from './_client.js';
+
+lemonSqueezySetup({
+ apiKey: env.billing.lemonSqueezy.apiKey,
+ onError: (error) => console.error('Error!', error),
+});
+
+export type SubscriptionTierType =
+ keyof typeof env.billing.lemonSqueezy.tierVariantId;
+
+export function getTierNameByvariantId(variantId: string) {
+ const tierName = Object.keys(env.billing.lemonSqueezy.tierVariantId).find(
+ (key) =>
+ env.billing.lemonSqueezy.tierVariantId[key as SubscriptionTierType] ===
+ variantId
+ );
+
+ if (!tierName) {
+ throw 'Unknown';
+ }
+
+ return tierName;
+}
+
+export function checkIsValidProduct(storeId: string, variantId: string) {
+ if (String(storeId) !== env.billing.lemonSqueezy.storeId) {
+ return false;
+ }
+
+ if (
+ !Object.values(env.billing.lemonSqueezy.tierVariantId).includes(variantId)
+ ) {
+ return false;
+ }
+
+ return true;
+}
+
+export async function createCheckoutBilling(
+ workspaceId: string,
+ userId: string,
+ subscriptionTier: SubscriptionTierType,
+ redirectUrl?: string
+) {
+ const variantId = env.billing.lemonSqueezy.tierVariantId[subscriptionTier];
+ if (!variantId) {
+ throw new Error('Unknown subscription tier');
+ }
+
+ const userInfo = await prisma.user.findUnique({
+ where: {
+ id: userId,
+ },
+ });
+
+ if (!userInfo) {
+ throw new Error('User not found');
+ }
+
+ const subscription = await prisma.lemonSqueezySubscription.findUnique({
+ where: {
+ workspaceId,
+ },
+ });
+
+ if (subscription) {
+ throw new Error('This workspace already has a subscription');
+ }
+
+ // not existed subscription
+ const checkout = await createCheckout(
+ env.billing.lemonSqueezy.storeId,
+ variantId,
+ {
+ checkoutData: {
+ name: userInfo.nickname ?? undefined,
+ email: userInfo.email ?? undefined,
+ custom: {
+ userId,
+ workspaceId,
+ },
+ },
+ productOptions: {
+ redirectUrl,
+ },
+ }
+ );
+
+ if (checkout.error) {
+ throw checkout.error;
+ }
+
+ const checkoutData = checkout.data.data;
+
+ return checkoutData;
+}
+
+export async function changeSubscription(
+ workspaceId: string,
+ subscriptionTier: SubscriptionTierType
+) {
+ const variantId = env.billing.lemonSqueezy.tierVariantId[subscriptionTier];
+ if (!variantId) {
+ throw new Error('Unknown subscription tier');
+ }
+
+ const subscription = await prisma.lemonSqueezySubscription.findUnique({
+ where: {
+ workspaceId,
+ },
+ });
+
+ if (!subscription) {
+ throw new Error('Can not found existed subscription');
+ }
+
+ const res = await updateSubscription(subscription.subscriptionId, {
+ variantId: Number(variantId),
+ });
+
+ if (res.error) {
+ throw res.error;
+ }
+
+ return res.data.data;
+}
+
+export async function cancelSubscription(workspaceId: string) {
+ const subscription = await prisma.lemonSqueezySubscription.findUnique({
+ where: {
+ workspaceId,
+ },
+ });
+
+ if (!subscription) {
+ throw new Error('Can not found existed subscription');
+ }
+
+ const res = await lsCancelSubscription(subscription.subscriptionId);
+
+ if (res.error) {
+ throw res.error;
+ }
+
+ return res.data.data;
+}
diff --git a/src/server/package.json b/src/server/package.json
index 446c9c8..1e7218a 100644
--- a/src/server/package.json
+++ b/src/server/package.json
@@ -26,6 +26,7 @@
"dependencies": {
"@auth/core": "^0.34.1",
"@auth/express": "^0.5.5",
+ "@lemonsqueezy/lemonsqueezy.js": "^3.3.1",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "5.14.0",
"@tianji/shared": "workspace:^",
diff --git a/src/server/prisma/migrations/20241003005451_add_lemon_squeezy/migration.sql b/src/server/prisma/migrations/20241003005451_add_lemon_squeezy/migration.sql
new file mode 100644
index 0000000..c8662df
--- /dev/null
+++ b/src/server/prisma/migrations/20241003005451_add_lemon_squeezy/migration.sql
@@ -0,0 +1,35 @@
+-- CreateTable
+CREATE TABLE "LemonSqueezySubscription" (
+ "subscriptionId" TEXT NOT NULL,
+ "workspaceId" VARCHAR(30) NOT NULL,
+ "storeId" TEXT NOT NULL,
+ "productId" TEXT NOT NULL,
+ "variantId" TEXT NOT NULL,
+ "status" TEXT NOT NULL,
+ "cardBrand" TEXT NOT NULL,
+ "cardLastFour" TEXT NOT NULL,
+ "renewsAt" TIMESTAMPTZ(6) NOT NULL,
+ "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMPTZ(6) NOT NULL,
+
+ CONSTRAINT "LemonSqueezySubscription_pkey" PRIMARY KEY ("subscriptionId")
+);
+
+-- CreateTable
+CREATE TABLE "LemonSqueezyWebhookEvent" (
+ "id" VARCHAR(30) NOT NULL,
+ "eventName" TEXT NOT NULL,
+ "payload" JSON NOT NULL,
+ "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "LemonSqueezyWebhookEvent_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "LemonSqueezySubscription_subscriptionId_key" ON "LemonSqueezySubscription"("subscriptionId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "LemonSqueezySubscription_workspaceId_key" ON "LemonSqueezySubscription"("workspaceId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "LemonSqueezyWebhookEvent_id_key" ON "LemonSqueezyWebhookEvent"("id");
diff --git a/src/server/prisma/schema.prisma b/src/server/prisma/schema.prisma
index 0905d22..7ea7b2a 100644
--- a/src/server/prisma/schema.prisma
+++ b/src/server/prisma/schema.prisma
@@ -128,6 +128,31 @@ model WorkspacesOnUsers {
@@index([workspaceId])
}
+model LemonSqueezySubscription {
+ subscriptionId String @id @unique
+ workspaceId String @unique @db.VarChar(30)
+ storeId String
+ productId String
+ variantId String
+ status String
+ cardBrand String
+ cardLastFour String
+ renewsAt DateTime @db.Timestamptz(6)
+
+ createdAt DateTime @default(now()) @db.Timestamptz(6)
+ updatedAt DateTime @updatedAt @db.Timestamptz(6)
+}
+
+model LemonSqueezyWebhookEvent {
+ id String @id @unique @default(cuid()) @db.VarChar(30)
+ eventName String
+ /// [CommonPayload]
+ /// @zod.custom(imports.CommonPayloadSchema)
+ payload Json @db.Json // Other payload info get from query params, should be a object
+
+ createdAt DateTime @default(now()) @db.Timestamptz(6)
+}
+
model Website {
id String @id @unique @default(cuid()) @db.VarChar(30)
workspaceId String @db.VarChar(30)
diff --git a/src/server/prisma/zod/index.ts b/src/server/prisma/zod/index.ts
index 825a053..f1285f4 100644
--- a/src/server/prisma/zod/index.ts
+++ b/src/server/prisma/zod/index.ts
@@ -5,6 +5,8 @@ export * from "./session.js"
export * from "./verificationtoken.js"
export * from "./workspace.js"
export * from "./workspacesonusers.js"
+export * from "./lemonsqueezysubscription.js"
+export * from "./lemonsqueezywebhookevent.js"
export * from "./website.js"
export * from "./websitesession.js"
export * from "./websiteevent.js"
diff --git a/src/server/prisma/zod/lemonsqueezysubscription.ts b/src/server/prisma/zod/lemonsqueezysubscription.ts
new file mode 100644
index 0000000..a0ce94b
--- /dev/null
+++ b/src/server/prisma/zod/lemonsqueezysubscription.ts
@@ -0,0 +1,16 @@
+import * as z from "zod"
+import * as imports from "./schemas/index.js"
+
+export const LemonSqueezySubscriptionModelSchema = z.object({
+ subscriptionId: z.string(),
+ workspaceId: z.string(),
+ storeId: z.string(),
+ productId: z.string(),
+ variantId: z.string(),
+ status: z.string(),
+ cardBrand: z.string(),
+ cardLastFour: z.string(),
+ renewsAt: z.date(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+})
diff --git a/src/server/prisma/zod/lemonsqueezytransaction.ts b/src/server/prisma/zod/lemonsqueezytransaction.ts
new file mode 100644
index 0000000..59e1bd8
--- /dev/null
+++ b/src/server/prisma/zod/lemonsqueezytransaction.ts
@@ -0,0 +1,13 @@
+import * as z from "zod"
+import * as imports from "./schemas/index.js"
+import { TransactionStatus } from "@prisma/client"
+
+export const LemonSqueezyTransactionModelSchema = z.object({
+ id: z.string(),
+ userId: z.string(),
+ workspaceId: z.string(),
+ checkoutId: z.string(),
+ status: z.nativeEnum(TransactionStatus),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+})
diff --git a/src/server/prisma/zod/lemonsqueezywebhookevent.ts b/src/server/prisma/zod/lemonsqueezywebhookevent.ts
new file mode 100644
index 0000000..e052ec1
--- /dev/null
+++ b/src/server/prisma/zod/lemonsqueezywebhookevent.ts
@@ -0,0 +1,18 @@
+import * as z from "zod"
+import * as imports from "./schemas/index.js"
+
+// Helper schema for JSON fields
+type Literal = boolean | number | string
+type Json = Literal | { [key: string]: Json } | Json[]
+const literalSchema = z.union([z.string(), z.number(), z.boolean()])
+const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
+
+export const LemonSqueezyWebhookEventModelSchema = z.object({
+ id: z.string(),
+ eventName: z.string(),
+ /**
+ * [CommonPayload]
+ */
+ payload: imports.CommonPayloadSchema,
+ createdAt: z.date(),
+})
diff --git a/src/server/router/billing.ts b/src/server/router/billing.ts
new file mode 100644
index 0000000..ab8f0a8
--- /dev/null
+++ b/src/server/router/billing.ts
@@ -0,0 +1,93 @@
+import { Router, raw } from 'express';
+import crypto from 'crypto';
+import { env } from '../utils/env.js';
+import { get } from 'lodash-es';
+import { checkIsValidProduct } from '../model/billing.js';
+import { prisma } from '../model/_client.js';
+import dayjs from 'dayjs';
+
+export const billingRouter = Router();
+
+billingRouter.post(
+ '/lemonsqueezy/webhook',
+ raw({
+ type: () => true,
+ }),
+ async (req, res) => {
+ const rawBody = String(req.rawBody);
+ const body = req.body;
+
+ signatureIsValid(rawBody, req.get('X-Signature') ?? '');
+
+ const eventName = get(body, 'meta.event_name');
+ const workspaceId = get(body, 'meta.custom_data.workspace_id');
+
+ await prisma.lemonSqueezyWebhookEvent.create({
+ data: {
+ eventName,
+ payload: body,
+ },
+ });
+
+ if (!workspaceId) {
+ res.status(500).send('No workspace id');
+ return;
+ }
+
+ if (eventName === 'subscription_updated') {
+ // update user subscription
+ const subscriptionId = String(get(body, 'data.id'));
+ const storeId = String(get(body, 'data.attributes.store_id'));
+ const productId = String(get(body, 'data.attributes.product_id'));
+ const variantId = String(get(body, 'data.attributes.variant_id'));
+ const status = get(body, 'data.attributes.status');
+ const cardBrand = get(body, 'data.attributes.card_brand');
+ const cardLastFour = get(body, 'data.attributes.card_last_four');
+ const renewsAt = dayjs(get(body, 'data.attributes.renews_at')).toDate();
+
+ if (!checkIsValidProduct(storeId, variantId)) {
+ throw new Error(`Invalid product: ${storeId}, ${variantId}`);
+ }
+
+ await prisma.lemonSqueezySubscription.upsert({
+ create: {
+ subscriptionId,
+ workspaceId,
+ storeId,
+ productId,
+ variantId,
+ status,
+ cardBrand,
+ cardLastFour,
+ renewsAt,
+ },
+ update: {
+ storeId,
+ productId,
+ variantId,
+ status,
+ cardBrand,
+ cardLastFour,
+ renewsAt,
+ },
+ where: {
+ workspaceId,
+ subscriptionId,
+ },
+ });
+ }
+
+ res.status(200).send('OK');
+ }
+);
+
+function signatureIsValid(rawBody: string, requestSignature: string) {
+ const secret = env.billing.lemonSqueezy.signatureSecret;
+ const hmac = crypto.createHmac('sha256', secret);
+ const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8');
+ const signature = Buffer.from(requestSignature, 'utf8');
+
+ if (!crypto.timingSafeEqual(digest, signature)) {
+ throw new Error('Invalid signature.');
+ }
+}
diff --git a/src/server/trpc/routers/billing.ts b/src/server/trpc/routers/billing.ts
index 9292390..7dfff55 100644
--- a/src/server/trpc/routers/billing.ts
+++ b/src/server/trpc/routers/billing.ts
@@ -1,8 +1,21 @@
import { z } from 'zod';
-import { OpenApiMetaInfo, router, workspaceProcedure } from '../trpc.js';
+import {
+ OpenApiMetaInfo,
+ router,
+ workspaceOwnerProcedure,
+ workspaceProcedure,
+} from '../trpc.js';
import { OPENAPI_TAG } from '../../utils/const.js';
import { prisma } from '../../model/_client.js';
import { OpenApiMeta } from 'trpc-openapi';
+import {
+ cancelSubscription,
+ changeSubscription,
+ createCheckoutBilling,
+ getTierNameByvariantId,
+ SubscriptionTierType,
+} from '../../model/billing.js';
+import { LemonSqueezySubscriptionModelSchema } from '../../prisma/zod/lemonsqueezysubscription.js';
export const billingRouter = router({
usage: workspaceProcedure
@@ -56,6 +69,87 @@ export const billingRouter = router({
feedEventCount: res._sum.feedEventCount ?? 0,
};
}),
+ currentSubscription: workspaceProcedure
+ .meta(
+ buildBillingOpenapi({
+ method: 'GET',
+ path: '/currentSubscription',
+ description: 'get workspace current subscription',
+ })
+ )
+ .output(
+ LemonSqueezySubscriptionModelSchema.merge(
+ z.object({
+ tier: z.string(),
+ })
+ ).nullable()
+ )
+ .query(async ({ input }) => {
+ const { workspaceId } = input;
+
+ const res = await prisma.lemonSqueezySubscription.findUnique({
+ where: {
+ workspaceId,
+ },
+ });
+
+ if (!res) {
+ return null;
+ }
+
+ return { ...res, tier: getTierNameByvariantId(res.variantId) };
+ }),
+ checkout: workspaceOwnerProcedure
+ .input(
+ z.object({
+ tier: z.string(),
+ redirectUrl: z.string().optional(),
+ })
+ )
+ .output(
+ z.object({
+ url: z.string(),
+ })
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { workspaceId, redirectUrl } = input;
+ const userId = ctx.user.id;
+ const checkout = await createCheckoutBilling(
+ workspaceId,
+ userId,
+ input.tier as SubscriptionTierType,
+ redirectUrl
+ );
+
+ const url = checkout.attributes.url;
+
+ return { url };
+ }),
+ changePlan: workspaceOwnerProcedure
+ .input(
+ z.object({
+ tier: z.string(),
+ })
+ )
+ .output(z.string())
+ .mutation(async ({ input }) => {
+ const { workspaceId } = input;
+
+ const subscription = await changeSubscription(
+ workspaceId,
+ input.tier as SubscriptionTierType
+ );
+
+ return subscription.id;
+ }),
+ cancelSubscription: workspaceOwnerProcedure
+ .output(z.string())
+ .mutation(async ({ input }) => {
+ const { workspaceId } = input;
+ const subscription = await cancelSubscription(workspaceId);
+
+ return subscription.id;
+ }),
});
function buildBillingOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
diff --git a/src/server/types/global.d.ts b/src/server/types/global.d.ts
index 9d66f61..3ab1530 100644
--- a/src/server/types/global.d.ts
+++ b/src/server/types/global.d.ts
@@ -8,6 +8,10 @@ import type {
declare global {
namespace Express {
interface User extends JWTPayload {}
+
+ interface Request {
+ rawBody: unknown;
+ }
}
namespace PrismaJson {
diff --git a/src/server/utils/env.ts b/src/server/utils/env.ts
index 99f2b3b..3369173 100644
--- a/src/server/utils/env.ts
+++ b/src/server/utils/env.ts
@@ -46,6 +46,18 @@ export const env = {
clientSecret: process.env.AUTH_CUSTOM_SECRET,
},
},
+ billing: {
+ lemonSqueezy: {
+ signatureSecret: process.env.LEMON_SQUEEZY_SIGNATURE_SECRET ?? '',
+ apiKey: process.env.LEMON_SQUEEZY_API_KEY ?? '',
+ storeId: process.env.LEMON_SQUEEZY_STORE_ID ?? '',
+ tierVariantId: {
+ free: process.env.LEMON_SQUEEZY_SUBSCRIPTION_FREE_ID ?? '',
+ pro: process.env.LEMON_SQUEEZY_SUBSCRIPTION_PRO_ID ?? '',
+ team: process.env.LEMON_SQUEEZY_SUBSCRIPTION_TEAM_ID ?? '',
+ },
+ },
+ },
allowRegister: checkEnvTrusty(process.env.ALLOW_REGISTER),
allowOpenapi: checkEnvTrusty(process.env.ALLOW_OPENAPI ?? 'true'),
websiteId: process.env.WEBSITE_ID,