Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2a3c956
chore: deploy api v2 on vercel
ThyMinimalDev Jan 12, 2026
d789511
fix: replace console.log with logger.log in Vercel handler
devin-ai-integration[bot] Jan 12, 2026
70f0def
chore: enable esModuleInterop
ThyMinimalDev Jan 12, 2026
554484b
Merge branch 'main' into deploy-api-v2-vercel
devin-ai-integration[bot] Jan 12, 2026
f200110
Merge branch 'deploy-api-v2-vercel' of https://git-manager.devin.ai/p…
devin-ai-integration[bot] Jan 12, 2026
6ac8ada
Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
45c704a
chore: deploy api v2 on vercel
ThyMinimalDev Jan 12, 2026
3b4d4f7
chore: deploy api v2 on vercel
ThyMinimalDev Jan 12, 2026
911d185
Update apps/api/v2/src/bootstrap.ts
ThyMinimalDev Jan 12, 2026
8a78551
Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
4959c88
fixup! Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
6b88ca0
Revert "chore: deploy api v2 on vercel"
ThyMinimalDev Jan 12, 2026
e81bcfd
chore: deploy api v2 on vercel
ThyMinimalDev Jan 12, 2026
f3474c2
fix: address Cubic AI review feedback in main.ts
devin-ai-integration[bot] Jan 12, 2026
89fb2b4
Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
7d26c97
fix: remove comma: true from qs.parse to maintain backward compatibility
devin-ai-integration[bot] Jan 12, 2026
0761b27
chore: deploy api v2 on vercel
ThyMinimalDev Jan 12, 2026
3aeaf83
Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
7a0b0c7
small fixes
ThyMinimalDev Jan 12, 2026
4b88ef0
chore: add try catch around bootstrap.ts
ThyMinimalDev Jan 12, 2026
f88b0d5
fix: use NestJS Logger and throw error instead of process.exit in boo…
devin-ai-integration[bot] Jan 12, 2026
ede63f9
chore: try log redis url
ThyMinimalDev Jan 12, 2026
67ed347
fix: sanitize REDIS_URL logging to avoid exposing credentials
devin-ai-integration[bot] Jan 12, 2026
98b3224
chore: remove unnecessary logs
ThyMinimalDev Jan 12, 2026
5bc5923
fix: prisma adapter
ThyMinimalDev Jan 12, 2026
198aebd
chore: handle USE_POOL platform libraries
ThyMinimalDev Jan 12, 2026
b23e109
Merge branch 'main' into deploy-api-v2-vercel
ThyMinimalDev Jan 12, 2026
621515b
fix: use JSON.stringify for Vite define value
devin-ai-integration[bot] Jan 12, 2026
e1b25f0
fix: docker file builds
ThyMinimalDev Jan 12, 2026
f0661dc
fix: correct Dockerfile build order for platform packages
devin-ai-integration[bot] Jan 12, 2026
97dfdae
fix: docker file builds
ThyMinimalDev Jan 12, 2026
865c076
Merge branch 'main' into deploy-api-v2-vercel
devin-ai-integration[bot] Jan 13, 2026
e287329
chore: add docker build
ThyMinimalDev Jan 13, 2026
83914bc
chore: upgrade nest/bull
ThyMinimalDev Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/api/v2/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ 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/platform-constants run build
RUN yarn workspace @calcom/platform-enums run build
RUN yarn workspace @calcom/platform-utils run build
RUN yarn workspace @calcom/platform-types run build
RUN yarn workspace @calcom/trpc run build:server
RUN yarn workspace @calcom/api-v2 run build

EXPOSE 80
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "UNLICENSED",
"private": true,
"scripts": {
"build": "yarn dev:build && nest build",
"build": "nest build",
"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\"",
Expand Down
114 changes: 60 additions & 54 deletions apps/api/v2/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

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,
});
const logger: Logger = new Logger("Bootstrap");

app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory(errors: ValidationError[]): BadRequestException {
return new BadRequestException({ errors });
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
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;
},
})
);
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;
};
87 changes: 76 additions & 11 deletions apps/api/v2/src/main.ts
Original file line number Diff line number Diff line change
@@ -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>);
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion apps/api/v2/src/modules/prisma/prisma-read.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/api/v2/src/modules/prisma/prisma-write.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
Expand Down
15 changes: 4 additions & 11 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand All @@ -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."
}
]
Expand Down
14 changes: 9 additions & 5 deletions packages/platform/libraries/vite.config.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading