chore: deploy api v2 on vercel (#26735)

* chore: deploy api v2 on vercel

* fix: replace console.log with logger.log in Vercel handler

Address Cubic AI review feedback to use the logging framework
consistently instead of console.log in the serverless handler.

Co-Authored-By: unknown <>

* chore: enable esModuleInterop

* chore: deploy api v2 on vercel

* chore: deploy api v2 on vercel

* Update apps/api/v2/src/bootstrap.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fixup! Merge branch 'main' into deploy-api-v2-vercel

* Revert "chore: deploy api v2 on vercel"

This reverts commit 45c704a48e8396c46118069e1a25d8d7a5ee84be.

* chore: deploy api v2 on vercel

* fix: address Cubic AI review feedback in main.ts

- Replace console.log with logger.log for consistent logging
- Replace console.error with logger.error for consistent error logging
- Restore comma: true option in qs.parse to support comma-separated arrays

Co-Authored-By: unknown <>

* fix: remove comma: true from qs.parse to maintain backward compatibility

The main branch does not have comma: true in the query parser, so adding
it would be a breaking change for existing API consumers. Removing it to
maintain consistency with the current production behavior.

Co-Authored-By: unknown <>

* chore: deploy api v2 on vercel

* small fixes

* chore: add try catch around bootstrap.ts

* fix: use NestJS Logger and throw error instead of process.exit in bootstrap

- Replace console.error with logger.error for consistent logging
- Replace process.exit(1) with throw error to avoid breaking Vercel serverless instance reuse

Addresses Cubic AI review feedback (confidence 10/10 for both issues)

Co-Authored-By: unknown <>

* chore: try log redis url

* fix: sanitize REDIS_URL logging to avoid exposing credentials

Replace full REDIS_URL logging with a boolean check that only indicates
whether Redis is configured, without exposing the connection string.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* chore: remove unnecessary logs

* fix: prisma adapter

* chore: handle USE_POOL platform libraries

* fix: use JSON.stringify for Vite define value

Wrap usePool with JSON.stringify() to properly serialize the string value.
Without this, Vite injects the raw value as an identifier instead of a
string literal, breaking runtime behavior.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* fix: docker file builds

* fix: correct Dockerfile build order for platform packages

Reorder builds to match the dependency graph from dev:build script:
constants → enums → utils → types → libraries → trpc → api-v2

platform-libraries depends on the other platform packages, so they
must be built first.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* fix: docker file builds

* chore: add docker build

* chore: upgrade nest/bull

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Morgan
2026-01-13 11:31:21 -03:00
committed by GitHub
co-authored by unknown <> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
parent bb9581d0d9
commit 9ad8aa981a
10 changed files with 202 additions and 109 deletions
+2 -1
View File
@@ -12,12 +12,13 @@ ENV NODE_ENV="production"
ENV NODE_OPTIONS="--max-old-space-size=8192"
ENV DATABASE_DIRECT_URL=${DATABASE_DIRECT_URL}
ENV DATABASE_URL=${DATABASE_URL}
ENV USE_POOL="true"
COPY . .
RUN yarn install
RUN yarn workspace @calcom/api-v2 run generate-schemas
RUN yarn workspace @calcom/platform-libraries run build
RUN yarn workspace @calcom/api-v2 run build:docker
RUN yarn workspace @calcom/api-v2 run build
EXPOSE 80
+3 -2
View File
@@ -6,7 +6,8 @@
"license": "UNLICENSED",
"private": true,
"scripts": {
"build": "yarn dev:build && nest build",
"build": "nest build",
"build:docker": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build && yarn workspace @calcom/trpc build:server",
"format": "biome format --write src test",
"start": "nest start",
"dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"",
@@ -47,7 +48,7 @@
"@microsoft/microsoft-graph-types-beta": "0.42.0-preview",
"@nest-lab/throttler-storage-redis": "1.0.0",
"@nestjs/axios": "4.0.0",
"@nestjs/bull": "10.1.1",
"@nestjs/bull": "11.0.4",
"@nestjs/common": "10.4.20",
"@nestjs/config": "3.2.0",
"@nestjs/core": "10.4.20",
+60 -54
View File
@@ -10,7 +10,7 @@ import {
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import type { ValidationError } from "@nestjs/common";
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
import { BadRequestException, Logger, ValidationPipe, VersioningType } from "@nestjs/common";
import type { NestExpressApplication } from "@nestjs/platform-express";
import cookieParser from "cookie-parser";
import { Request } from "express";
@@ -21,62 +21,68 @@ import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
const logger: Logger = new Logger("Bootstrap");
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
app.enableShutdownHooks();
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
return headerVersion;
}
return VERSION_2024_04_15;
},
defaultVersion: VERSION_2024_04_15,
});
app.use(helmet());
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
"Content-Type",
"Origin",
],
maxAge: 86_400,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
validationError: {
target: true,
value: true,
try {
if (!process.env.VERCEL) {
app.enableShutdownHooks();
}
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
return headerVersion;
}
return VERSION_2024_04_15;
},
exceptionFactory(errors: ValidationError[]): BadRequestException {
return new BadRequestException({ errors });
},
})
);
defaultVersion: VERSION_2024_04_15,
});
app.use(helmet());
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
"Content-Type",
"Origin",
],
maxAge: 86_400,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory(errors: ValidationError[]): BadRequestException {
return new BadRequestException({ errors });
},
})
);
// Exception filters, new filters go at the bottom, keep the order
app.useGlobalFilters(new PrismaExceptionFilter());
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
// Exception filters, new filters go at the bottom, keep the order
app.useGlobalFilters(new PrismaExceptionFilter());
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
if (process?.env?.API_GLOBAL_PREFIX) {
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
}
app.use(cookieParser());
if (process?.env?.API_GLOBAL_PREFIX) {
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
return app;
} catch (error) {
logger.error("Error starting NestJS app:", error);
throw error;
}
return app;
};
+76 -11
View File
@@ -1,26 +1,64 @@
import "dotenv/config";
import { IncomingMessage, Server, ServerResponse } from "node:http";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import type { Express, Request, Response } from "express";
import { WinstonModule } from "nest-winston";
import qs from "qs";
import type { AppConfig } from "@/config/type";
import { AppModule } from "./app.module";
import { bootstrap } from "./bootstrap";
import { loggerConfig } from "./lib/logger";
run().catch((error: Error) => {
console.error("Failed to start Cal Platform API", { error: error.stack });
process.exit(1);
});
const logger: Logger = new Logger("App");
/**
* Singleton Class to manage the NestJS App instance.
* Ensures we only initialize the app once per container lifecycle.
*/
class NestServer {
private static server: Express; // The underlying Express instance
private constructor() {}
/**
* Returns the cached server instance.
* If it doesn't exist, it creates, bootstraps, and initializes it.
*/
public static async getInstance(): Promise<Express> {
if (!NestServer.server) {
const app = await createNestApp();
// Execute bootstrap (Pipes, Interceptors, CORS, etc.)
bootstrap(app);
// Initialize the app (connects to DB, resolves modules)
await app.init();
// extract the Express instance to pass to Vercel
NestServer.server = app.getHttpAdapter().getInstance();
}
return NestServer.server;
}
}
// -----------------------------------------------------------------------------
// LOCAL DEVELOPMENT STARTUP
// -----------------------------------------------------------------------------
if (!process.env.VERCEL) {
run().catch((error: Error) => {
logger.error("Failed to start Cal Platform API", { error: error.stack });
process.exit(1);
});
}
async function run(): Promise<void> {
const app = await createNestApp();
const logger = new Logger("App");
try {
bootstrap(app);
const config = app.get(ConfigService<AppConfig, true>);
@@ -32,23 +70,50 @@ async function run(): Promise<void> {
}
await app.listen(port);
logger.log(`Application started on port: ${port}`);
logger.log(`Application started locally on port: ${port}`);
} catch (error) {
console.error(error);
logger.error("Application crashed", {
error,
});
logger.error("Application crashed during local startup", { error });
}
}
// -----------------------------------------------------------------------------
// VERCEL SERVERLESS HANDLER
// -----------------------------------------------------------------------------
export default async (req: Request, res: Response): Promise<void> => {
try {
const server = await NestServer.getInstance();
// Vercel/AWS specific: Re-parse query strings to support array formats
// (e.g., ?ids[]=1&ids[]=2) which Vercel's native parser might simplify.
if (req.url) {
const [_path, queryString] = req.url.split("?");
if (queryString) {
req.query = qs.parse(queryString, { arrayLimit: 1000 });
}
}
// Delegate request to the cached Express instance
return server(req, res);
} catch (error) {
logger.error("Critical: Failed to initialize NestJS Serverless instance", error);
res.statusCode = 500;
res.end("Internal Server Error: Initialization Failed");
}
};
// -----------------------------------------------------------------------------
// APP FACTORY
// -----------------------------------------------------------------------------
export async function createNestApp(): Promise<
NestExpressApplication<Server<typeof IncomingMessage, typeof ServerResponse>>
> {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(loggerConfig()),
// Preserved as requested:
bodyParser: false,
});
// Custom query parser configuration for the underlying Express app
app.set("query parser", (str: string) => qs.parse(str, { arrayLimit: 1000 }));
return app;
@@ -54,8 +54,9 @@ export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
const adapter = new PrismaPg(this.pool);
this.prisma = new PrismaClient({ adapter });
} else {
const adapter = new PrismaPg({ connectionString: dbUrl });
this.prisma = new PrismaClient({
datasourceUrl: dbUrl,
adapter,
});
}
}
@@ -60,8 +60,9 @@ export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
const adapter = new PrismaPg(this.pool);
this.prisma = new PrismaClient({ adapter });
} else {
const adapter = new PrismaPg({ connectionString: dbUrl });
this.prisma = new PrismaClient({
datasourceUrl: dbUrl,
adapter,
});
}
}
+4 -11
View File
@@ -164,6 +164,7 @@
{
"includes": [
"apps/api/v2/src/config/app.ts",
"apps/api/v2/src/main.ts",
"apps/api/v2/src/bootstrap.ts",
"apps/api/v2/src/config/env.ts",
"apps/api/v2/src/env.ts",
@@ -325,9 +326,7 @@
}
},
{
"includes": [
"packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"
],
"includes": ["packages/platform/atoms/**/*.{ts,tsx,js,jsx,mts,mjs,cjs,cts}"],
"linter": {
"rules": {
"style": {
@@ -336,17 +335,11 @@
"options": {
"patterns": [
{
"group": [
"@calcom/trpc",
"@calcom/trpc/**"
],
"group": ["@calcom/trpc", "@calcom/trpc/**"],
"message": "atoms package should not import from @calcom/trpc."
},
{
"group": [
"../../trpc",
"../../trpc/**"
],
"group": ["../../trpc", "../../trpc/**"],
"message": "atoms package should not import from trpc."
}
]
+9 -5
View File
@@ -1,19 +1,23 @@
// vite.config.ts
import react from "@vitejs/plugin-react";
import { resolve } from "node:path";
import path from "node:path"
import { dirname } from "node:path";
import path, { dirname, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const usePool = process.env.USE_POOL ?? "true";
console.log("Platform libraries usePool", usePool);
// https://vitejs.dev/guide/build.html#library-mode
export default defineConfig({
define: {
"process.env.USE_POOL": `"true"`,
"process.env.USE_POOL": JSON.stringify(usePool),
},
esbuild: {
target: "node18",
+28
View File
@@ -292,6 +292,33 @@
"LINKEDIN_ADS_ENABLED",
"SEED_PLATFORM_OAUTH_CLIENT_ID",
"SEED_PLATFORM_OAUTH_CLIENT_SECRET",
"API_PORT",
"API_ENV",
"API_URL",
"DATABASE_WRITE_URL",
"JWT_SECRET",
"DOCS_URL",
"DATABASE_READ_URL",
"GET_LICENSE_KEY_URL",
"LOG_LEVEL",
"RATE_LIMIT_DEFAULT_TTL_MS",
"NEXTAUTH_SECRET_BACKUP",
"RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN",
"RATE_LIMIT_DEFAULT_LIMIT",
"RATE_LIMIT_DEFAULT_LIMIT_API_KEY",
"RATE_LIMIT_DEFAULT_LIMIT_OAUTH_CLIENT",
"STRIPE_API_KEY",
"STRIPE_PRICE_ID_SCALE",
"STRIPE_PRICE_ID_ESSENTIALS_OVERAGE",
"REPLEXICA_API_KEY",
"SLOTS_WORKER_POOL_SIZE",
"STRIPE_PRICE_ID_STARTER",
"STRIPE_PRICE_ID_SCALE_OVERAGE",
"STRIPE_PRICE_ID_STARTER_OVERAGE",
"STRIPE_PRICE_ID_ESSENTIALS",
"WEB_APP_URL",
"REDIS_URL",
"ENABLE_SLOTS_WORKERS",
"B2_APPLICATION_KEY_ID",
"B2_APPLICATION_KEY",
"B2_BUCKET_ID",
@@ -382,6 +409,7 @@
},
"@calcom/api-v2#build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": [
"NODE_ENV",
"API_PORT",
+16 -23
View File
@@ -1935,7 +1935,7 @@ __metadata:
"@microsoft/microsoft-graph-types-beta": "npm:0.42.0-preview"
"@nest-lab/throttler-storage-redis": "npm:1.0.0"
"@nestjs/axios": "npm:4.0.0"
"@nestjs/bull": "npm:10.1.1"
"@nestjs/bull": "npm:11.0.4"
"@nestjs/cli": "npm:10.3.2"
"@nestjs/common": "npm:10.4.20"
"@nestjs/config": "npm:3.2.0"
@@ -8209,29 +8209,29 @@ __metadata:
languageName: node
linkType: hard
"@nestjs/bull-shared@npm:^10.1.1":
version: 10.1.1
resolution: "@nestjs/bull-shared@npm:10.1.1"
"@nestjs/bull-shared@npm:^11.0.4":
version: 11.0.4
resolution: "@nestjs/bull-shared@npm:11.0.4"
dependencies:
tslib: "npm:2.6.2"
tslib: "npm:2.8.1"
peerDependencies:
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0
"@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0
checksum: 10/d8eb7747e6e5d0e8cc7ebd1c9824fc6a99bdf5798ab3b192a60643a4fb6bf3c352d336990b52c7437a00265724e7fb1449a57aec6e9d82bb35bdb898e2fe10c1
"@nestjs/common": ^10.0.0 || ^11.0.0
"@nestjs/core": ^10.0.0 || ^11.0.0
checksum: 10/dd8c9e93805e7e44b478e39652d513bbdba8b445491c0ebc6c7e5d03c48cb08a085c9a21ffa24091215c0a03f2977162310f4218d547cc029b28c8649d801060
languageName: node
linkType: hard
"@nestjs/bull@npm:10.1.1":
version: 10.1.1
resolution: "@nestjs/bull@npm:10.1.1"
"@nestjs/bull@npm:11.0.4":
version: 11.0.4
resolution: "@nestjs/bull@npm:11.0.4"
dependencies:
"@nestjs/bull-shared": "npm:^10.1.1"
tslib: "npm:2.6.2"
"@nestjs/bull-shared": "npm:^11.0.4"
tslib: "npm:2.8.1"
peerDependencies:
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0
"@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
"@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
bull: ^3.3 || ^4.0.0
checksum: 10/8b97a3f6b12927a22dd0fcfbebeaeb978f8e783da084fd50d79b34b72570593b7ee926f94fdf8e50eca3530282227ac4abe759e0bdd555fbbf2dda4f54b5a0ae
checksum: 10/6338e53537789b0aef6a9558c3baedfef04ab96c4442a22afebfc1ac9da9951e9b49c5b1ccef3d56009b7ddf0dec59a59e17663aa01a3171eebca13e791feb58
languageName: node
linkType: hard
@@ -37370,13 +37370,6 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.6.2":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
languageName: node
linkType: hard
"tslib@npm:2.8.1, tslib@npm:^2, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"