diff --git a/apps/web/components/apps/AppSetupPage.tsx b/apps/web/components/apps/AppSetupPage.tsx
index 88acf8c89bbb76..0110bdce792537 100644
--- a/apps/web/components/apps/AppSetupPage.tsx
+++ b/apps/web/components/apps/AppSetupPage.tsx
@@ -2,6 +2,7 @@ import { DynamicComponent } from "@calcom/app-store/_components/DynamicComponent
import dynamic from "next/dynamic";
export const AppSetupMap = {
+ bigbluebutton: dynamic(() => import("@calcom/web/components/apps/bigbluebutton/Setup")),
alby: dynamic(() => import("@calcom/web/components/apps/alby/Setup")),
"apple-calendar": dynamic(() => import("@calcom/web/components/apps/applecalendar/Setup")),
exchange: dynamic(() => import("@calcom/web/components/apps/exchangecalendar/Setup")),
diff --git a/apps/web/components/apps/bigbluebutton/Setup.tsx b/apps/web/components/apps/bigbluebutton/Setup.tsx
new file mode 100644
index 00000000000000..b182702603959f
--- /dev/null
+++ b/apps/web/components/apps/bigbluebutton/Setup.tsx
@@ -0,0 +1,95 @@
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { Toaster } from "sonner";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Alert } from "@calcom/ui/components/alert";
+import { Button } from "@calcom/ui/components/button";
+import { Form } from "@calcom/ui/components/form";
+import { TextField } from "@calcom/ui/components/form";
+import { PasswordField } from "@calcom/ui/components/form";
+
+export default function BigBlueButtonSetup() {
+ const { t } = useLocale();
+ const router = useRouter();
+ const form = useForm({
+ defaultValues: {
+ serverUrl: "",
+ sharedSecret: "",
+ },
+ });
+
+ const [errorMessage, setErrorMessage] = useState("");
+
+ return (
+
+
+
+
+ {/* eslint-disable @next/next/no-img-element */}
+

+
+
+
Connect BigBlueButton
+
+ Enter your BigBlueButton server URL and shared secret to enable video conferencing.
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts
index 5df66d19f7411c..658ddebe623381 100644
--- a/packages/app-store/apps.keys-schemas.generated.ts
+++ b/packages/app-store/apps.keys-schemas.generated.ts
@@ -22,6 +22,7 @@ import { appKeysSchema as insihts_zod_ts } from "./insihts/zod";
import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
import { appKeysSchema as jelly_zod_ts } from "./jelly/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
+import { appKeysSchema as bigbluebutton_zod_ts } from "./bigbluebutton/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as lyra_zod_ts } from "./lyra/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
@@ -74,6 +75,7 @@ export const appKeysSchemas = {
intercom: intercom_zod_ts,
jelly: jelly_zod_ts,
jitsivideo: jitsivideo_zod_ts,
+ bigbluebutton: bigbluebutton_zod_ts,
larkcalendar: larkcalendar_zod_ts,
lyra: lyra_zod_ts,
make: make_zod_ts,
diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts
index 4857b87fdac178..7e085e66dd4c16 100644
--- a/packages/app-store/apps.metadata.generated.ts
+++ b/packages/app-store/apps.metadata.generated.ts
@@ -52,6 +52,7 @@ import insihts_config_json from "./insihts/config.json";
import intercom_config_json from "./intercom/config.json";
import jelly_config_json from "./jelly/config.json";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
+import { metadata as bigbluebutton__metadata_ts } from "./bigbluebutton/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import lindy_config_json from "./lindy/config.json";
import linear_config_json from "./linear/config.json";
@@ -164,6 +165,7 @@ export const appStoreMetadata = {
intercom: intercom_config_json,
jelly: jelly_config_json,
jitsivideo: jitsivideo__metadata_ts,
+ bigbluebutton: bigbluebutton__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
lindy: lindy_config_json,
linear: linear_config_json,
diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts
index 495e07786d6754..8584116642acba 100644
--- a/packages/app-store/apps.server.generated.ts
+++ b/packages/app-store/apps.server.generated.ts
@@ -38,6 +38,7 @@ export const apiHandlers = {
intercom: import("./intercom/api"),
jelly: import("./jelly/api"),
jitsivideo: import("./jitsivideo/api"),
+ bigbluebutton: import("./bigbluebutton/api"),
larkcalendar: import("./larkcalendar/api"),
linear: import("./linear/api"),
lyra: import("./lyra/api"),
diff --git a/packages/app-store/bigbluebutton/DESCRIPTION.md b/packages/app-store/bigbluebutton/DESCRIPTION.md
new file mode 100644
index 00000000000000..166f78e7f1c076
--- /dev/null
+++ b/packages/app-store/bigbluebutton/DESCRIPTION.md
@@ -0,0 +1,44 @@
+---
+items:
+ - icon.svg
+---
+
+BigBlueButton is an open-source video conferencing system designed for education and business. Host unlimited meetings on your own server with features like screen sharing, whiteboard, breakout rooms, and recording capabilities.
+
+## Features
+
+- 🎥 HD video conferencing
+- 📱 Works on desktop and mobile
+- 🎯 Built for education and business
+- 🔒 Self-hosted for privacy and control
+- 📊 Screen sharing and presentation tools
+- ✏️ Interactive whiteboard
+- 👥 Breakout rooms for small groups
+- 📹 Optional meeting recording
+
+## Setup
+
+1. **BigBlueButton Server**: You need access to a BigBlueButton server. You can:
+ - Set up your own server following the [BigBlueButton installation guide](https://docs.bigbluebutton.org/install/install-overview)
+ - Use a hosted BigBlueButton provider
+ - Use BigBlueButton's demo server for testing (not recommended for production)
+
+2. **Server URL**: Enter your BigBlueButton server URL (e.g., `https://your-bbb-server.com`)
+
+3. **Shared Secret**: Enter your BigBlueButton shared secret (found in `/opt/bbb/conf/salt` on your server)
+
+## How it works
+
+When someone books a meeting with you:
+
+1. A unique BigBlueButton meeting room is automatically created
+2. The meeting host (you) receives a moderator link
+3. Meeting attendees receive participant links
+4. The meeting room is automatically deleted when the booking is cancelled
+
+## Security
+
+- All communications are encrypted end-to-end
+- Meeting rooms are created on-demand and deleted after use
+- Attendee access is controlled by unique passwords
+- Self-hosted deployment keeps your data private
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/_metadata.ts b/packages/app-store/bigbluebutton/_metadata.ts
new file mode 100644
index 00000000000000..49b52cab166d14
--- /dev/null
+++ b/packages/app-store/bigbluebutton/_metadata.ts
@@ -0,0 +1,30 @@
+import type { AppMeta } from "@calcom/types/App";
+
+import _package from "./package.json";
+
+export const metadata = {
+ name: "BigBlueButton",
+ description: _package.description,
+ type: "bigbluebutton_video",
+ variant: "conferencing",
+ categories: ["conferencing"],
+ logo: "icon.svg",
+ publisher: "Cal.com",
+ url: "https://bigbluebutton.org/",
+ slug: "bigbluebutton",
+ title: "BigBlueButton",
+ isGlobal: false,
+ email: "help@cal.com",
+ appData: {
+ location: {
+ linkType: "dynamic",
+ type: "integrations:bigbluebutton",
+ label: "BigBlueButton",
+ },
+ },
+ dirName: "bigbluebutton",
+ concurrentMeetings: true,
+ isOAuth: false,
+} as AppMeta;
+
+export default metadata;
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/api/add.ts b/packages/app-store/bigbluebutton/api/add.ts
new file mode 100644
index 00000000000000..085d59b07be205
--- /dev/null
+++ b/packages/app-store/bigbluebutton/api/add.ts
@@ -0,0 +1,114 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
+import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
+import prisma from "@calcom/prisma";
+import getInstalledAppPath from "../../_utils/getInstalledAppPath";
+import { BBBApi } from "../lib/bbb-api";
+
+/**
+ * BigBlueButton app installation endpoint
+ * Validates BBB server connection before saving credentials
+ */
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (!req.session?.user?.id) {
+ return res.status(401).json({ message: "You must be logged in to do this" });
+ }
+
+ if (req.method !== "POST") {
+ return res.status(405).json({ message: "Method not allowed" });
+ }
+
+ const { teamId, returnTo } = req.query;
+ const { serverUrl, sharedSecret } = req.body;
+
+ // Validate required fields
+ if (!serverUrl || !sharedSecret) {
+ return res.status(400).json({
+ message: "Server URL and Shared Secret are required"
+ });
+ }
+
+ // Validate server URL format
+ try {
+ const url = new URL(serverUrl);
+ if (!url.protocol.startsWith("http")) {
+ throw new Error("Invalid protocol");
+ }
+ } catch {
+ return res.status(400).json({
+ message: "Invalid server URL format. Please provide a valid HTTP or HTTPS URL."
+ });
+ }
+
+ await throwIfNotHaveAdminAccessToTeam({
+ teamId: teamId ? Number(teamId) : null,
+ userId: req.session.user.id,
+ });
+
+ const installForObject = teamId ? { teamId: Number(teamId) } : { userId: req.session.user.id };
+ const appType = "bigbluebutton_video";
+
+ try {
+ // Test BBB server connection first before attempting to install
+ const bbbApi = new BBBApi({ serverUrl, sharedSecret });
+ const isConnected = await bbbApi.testConnection();
+
+ if (!isConnected) {
+ return res.status(400).json({
+ message: "Could not connect to BigBlueButton server. Please verify the server URL and shared secret."
+ });
+ }
+
+ // Use a transaction to prevent race conditions
+ const installation = await prisma.$transaction(async (tx) => {
+ // Check for existing installation within transaction
+ const existing = await tx.credential.findFirst({
+ where: {
+ type: appType,
+ ...installForObject,
+ },
+ });
+
+ if (existing) {
+ // Update existing installation instead of creating duplicate
+ return await tx.credential.update({
+ where: { id: existing.id },
+ data: {
+ key: {
+ serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes
+ sharedSecret,
+ },
+ },
+ });
+ }
+
+ // Create new installation if none exists
+ return await tx.credential.create({
+ data: {
+ type: appType,
+ key: {
+ serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes
+ sharedSecret,
+ },
+ ...installForObject,
+ appId: "bigbluebutton",
+ },
+ });
+ });
+
+ if (!installation) {
+ throw new Error("Unable to create BigBlueButton credential");
+ }
+
+ return res.status(200).json({
+ url: returnTo ?? getInstalledAppPath({ variant: "conferencing", slug: "bigbluebutton" }),
+ message: "BigBlueButton successfully configured!"
+ });
+
+ } catch (error: unknown) {
+ // Log safe error message without sensitive credentials
+ console.error("BigBlueButton installation error:", error instanceof Error ? error.message : "Unknown error");
+ const httpError = getServerErrorFromUnknown(error);
+ return res.status(httpError.statusCode).json({ message: httpError.message });
+ }
+}
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/api/index.ts b/packages/app-store/bigbluebutton/api/index.ts
new file mode 100644
index 00000000000000..9320291f2764dd
--- /dev/null
+++ b/packages/app-store/bigbluebutton/api/index.ts
@@ -0,0 +1 @@
+export { default as add } from "./add";
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/index.ts b/packages/app-store/bigbluebutton/index.ts
new file mode 100644
index 00000000000000..02c2de5f5cc282
--- /dev/null
+++ b/packages/app-store/bigbluebutton/index.ts
@@ -0,0 +1,3 @@
+export * as lib from "./lib";
+export * as api from "./api";
+export { metadata } from "./_metadata";
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts b/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts
new file mode 100644
index 00000000000000..55d945131c2f64
--- /dev/null
+++ b/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts
@@ -0,0 +1,175 @@
+import { v4 as uuidv4 } from "uuid";
+import type { CalendarEvent } from "@calcom/types/Calendar";
+import type { PartialReference } from "@calcom/types/EventManager";
+import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
+
+import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
+import { metadata } from "../_metadata";
+import { BBBApi } from "./bbb-api";
+
+const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => {
+ return {
+ getAvailability: () => {
+ return Promise.resolve([]);
+ },
+
+ createMeeting: async (eventData: CalendarEvent): Promise => {
+ const appKeys = await getAppKeysFromSlug(metadata.slug);
+
+ const serverUrl = appKeys.serverUrl as string;
+ const sharedSecret = appKeys.sharedSecret as string;
+
+ if (!serverUrl || !sharedSecret) {
+ throw new Error("BigBlueButton configuration missing. Please configure server URL and shared secret.");
+ }
+
+ // Generate unique meeting ID and passwords
+ const meetingID = `cal-${uuidv4()}`;
+ const moderatorPassword = uuidv4().substring(0, 8);
+ const attendeePassword = uuidv4().substring(0, 8);
+
+ // Create meeting name
+ const meetingName = eventData.title || "Cal.com Meeting";
+
+ try {
+ const bbbApi = new BBBApi({ serverUrl, sharedSecret });
+
+ // Create the meeting
+ await bbbApi.createMeeting({
+ meetingID,
+ name: meetingName,
+ moderatorPW: moderatorPassword,
+ attendeePW: attendeePassword,
+ welcome: `Welcome to ${meetingName}! Please wait for the host to start the meeting.`,
+ maxParticipants: 50,
+ });
+
+ // Generate attendee join URL as the canonical meeting URL (safe for sharing)
+ const attendeeJoinUrl = bbbApi.getJoinUrl({
+ meetingID,
+ fullName: "Participant",
+ password: attendeePassword,
+ userID: "participant",
+ role: "viewer",
+ });
+
+ return {
+ type: metadata.type,
+ id: `${meetingID}|${moderatorPassword}|${serverUrl}`, // Store structured meeting data
+ password: attendeePassword, // Standard semantic: password for meeting access
+ url: attendeeJoinUrl, // Safe attendee URL as canonical URL
+ };
+ } catch (error) {
+ console.error("Failed to create BigBlueButton meeting:", error);
+ throw new Error(
+ error instanceof Error
+ ? `BigBlueButton meeting creation failed: ${error.message}`
+ : "Failed to create BigBlueButton meeting"
+ );
+ }
+ },
+
+ deleteMeeting: async (bookingRef: PartialReference): Promise => {
+ if (!bookingRef.meetingId) {
+ return;
+ }
+
+ try {
+ // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl
+ const idParts = bookingRef.meetingId.split("|");
+ if (idParts.length !== 3) {
+ console.warn("Invalid BigBlueButton meeting ID format for deletion");
+ return;
+ }
+
+ const [actualMeetingId, moderatorPassword, storedServerUrl] = idParts;
+
+ // Get app keys for shared secret (server URL comes from meeting ID)
+ const appKeys = await getAppKeysFromSlug(metadata.slug);
+ const sharedSecret = appKeys.sharedSecret as string;
+
+ if (!sharedSecret) {
+ console.warn("BigBlueButton shared secret missing for meeting deletion");
+ return;
+ }
+
+ const bbbApi = new BBBApi({
+ serverUrl: storedServerUrl,
+ sharedSecret
+ });
+
+ await bbbApi.endMeeting(actualMeetingId, moderatorPassword);
+ } catch (error) {
+ console.error("Failed to end BigBlueButton meeting:", error);
+ // Don't throw error for deletion failures to avoid blocking booking cancellation
+ }
+ },
+
+ updateMeeting: (bookingRef: PartialReference): Promise => {
+ // For BBB, we just return the existing meeting info
+ // BBB meetings are stateless and don't need updating
+
+ const meetingId = bookingRef.meetingId as string;
+ const attendeePassword = bookingRef.meetingPassword as string;
+ const meetingUrl = bookingRef.meetingUrl as string;
+
+ return Promise.resolve({
+ type: metadata.type,
+ id: meetingId,
+ password: attendeePassword, // Standard attendee password
+ url: meetingUrl,
+ });
+ },
+
+ /**
+ * Generate attendee join URL for guests
+ */
+ getGuestJoinUrl: async (
+ structuredMeetingId: string,
+ guestName: string,
+ attendeePassword: string
+ ): Promise => {
+ // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl
+ const [actualMeetingId, , serverUrl] = structuredMeetingId.split("|");
+
+ const appKeys = await getAppKeysFromSlug(metadata.slug);
+ const sharedSecret = appKeys.sharedSecret as string;
+
+ const bbbApi = new BBBApi({ serverUrl, sharedSecret });
+
+ return bbbApi.getJoinUrl({
+ meetingID: actualMeetingId,
+ fullName: guestName || "Guest",
+ password: attendeePassword,
+ userID: `guest-${Date.now()}`,
+ role: "viewer",
+ });
+ },
+
+ /**
+ * Generate moderator join URL for organizers
+ */
+ getModeratorJoinUrl: async (
+ structuredMeetingId: string,
+ organizerName: string
+ ): Promise => {
+ // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl
+ const [actualMeetingId, moderatorPassword, serverUrl] = structuredMeetingId.split("|");
+
+ const appKeys = await getAppKeysFromSlug(metadata.slug);
+ const sharedSecret = appKeys.sharedSecret as string;
+
+ const bbbApi = new BBBApi({ serverUrl, sharedSecret });
+
+ return bbbApi.getJoinUrl({
+ meetingID: actualMeetingId,
+ fullName: organizerName || "Host",
+ password: moderatorPassword,
+ userID: "moderator",
+ role: "moderator",
+ });
+ },
+ };
+};
+
+export default BigBlueButtonVideoApiAdapter;
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/lib/bbb-api.ts b/packages/app-store/bigbluebutton/lib/bbb-api.ts
new file mode 100644
index 00000000000000..742d0c49ccc9b0
--- /dev/null
+++ b/packages/app-store/bigbluebutton/lib/bbb-api.ts
@@ -0,0 +1,177 @@
+import crypto from "crypto";
+import axios from "axios";
+
+interface BBBCredentials {
+ serverUrl: string;
+ sharedSecret: string;
+}
+
+interface BBBMeetingParams {
+ meetingID: string;
+ name: string;
+ moderatorPW: string;
+ attendeePW: string;
+ welcome?: string;
+ logoutURL?: string;
+ maxParticipants?: number;
+}
+
+interface BBBJoinParams {
+ meetingID: string;
+ fullName: string;
+ password: string;
+ userID?: string;
+ role?: "moderator" | "viewer";
+}
+
+export class BBBApi {
+ private serverUrl: string;
+ private sharedSecret: string;
+
+ constructor(credentials: BBBCredentials) {
+ // Ensure server URL doesn't end with slash
+ this.serverUrl = credentials.serverUrl.replace(/\/+$/, "");
+ this.sharedSecret = credentials.sharedSecret;
+ }
+
+ /**
+ * Generate SHA-256 checksum for BBB API authentication
+ */
+ private generateChecksum(queryString: string): string {
+ const checksumString = queryString + this.sharedSecret;
+ return crypto.createHash("sha256").update(checksumString).digest("hex");
+ }
+
+ /**
+ * Build authenticated BBB API URL
+ */
+ private buildApiUrl(apiCall: string, params: Record): string {
+ // Remove undefined/null values and convert to string
+ const cleanParams = Object.entries(params)
+ .filter(([_, value]) => value !== undefined && value !== null)
+ .reduce((acc, [key, value]) => {
+ acc[key] = String(value);
+ return acc;
+ }, {} as Record);
+
+ // Build query string
+ const queryString = new URLSearchParams(cleanParams).toString();
+
+ // Generate checksum
+ const checksum = this.generateChecksum(apiCall + queryString);
+
+ // Build final URL
+ return `${this.serverUrl}/bigbluebutton/api/${apiCall}?${queryString}&checksum=${checksum}`;
+ }
+
+ /**
+ * Create a BigBlueButton meeting
+ */
+ async createMeeting(params: BBBMeetingParams): Promise {
+ const apiParams = {
+ meetingID: params.meetingID,
+ name: params.name,
+ moderatorPW: params.moderatorPW,
+ attendeePW: params.attendeePW,
+ welcome: params.welcome || `Welcome to ${params.name}!`,
+ logoutURL: params.logoutURL,
+ maxParticipants: params.maxParticipants || 100,
+ autoStartRecording: false,
+ allowStartStopRecording: false,
+ record: false,
+ duration: 0, // No time limit
+ };
+
+ const url = this.buildApiUrl("create", apiParams);
+
+ try {
+ const response = await axios.get(url, {
+ timeout: 10000, // 10 second timeout
+ });
+
+ // BBB returns XML, check for errors
+ if (response.data.includes("FAILED")) {
+ const messageMatch = response.data.match(/(.*?)<\/message>/);
+ const message = messageMatch ? messageMatch[1] : "Failed to create meeting";
+ throw new Error(`BBB API Error: ${message}`);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Failed to create BBB meeting: ${error.message}`);
+ }
+ throw new Error("Failed to create BBB meeting");
+ }
+ }
+
+ /**
+ * Generate join URL for a participant
+ */
+ getJoinUrl(params: BBBJoinParams): string {
+ const apiParams = {
+ meetingID: params.meetingID,
+ fullName: params.fullName,
+ password: params.password,
+ userID: params.userID,
+ redirect: "true", // Redirect immediately to meeting
+ };
+
+ return this.buildApiUrl("join", apiParams);
+ }
+
+ /**
+ * End a BigBlueButton meeting
+ */
+ async endMeeting(meetingID: string, moderatorPassword: string): Promise {
+ const apiParams = {
+ meetingID,
+ password: moderatorPassword,
+ };
+
+ const url = this.buildApiUrl("end", apiParams);
+
+ try {
+ const response = await axios.get(url, {
+ timeout: 10000,
+ });
+
+ if (response.data.includes("FAILED")) {
+ const messageMatch = response.data.match(/(.*?)<\/message>/);
+ const message = messageMatch ? messageMatch[1] : "Failed to end meeting";
+ throw new Error(`BBB API Error: ${message}`);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Failed to end BBB meeting: ${error.message}`);
+ }
+ throw new Error("Failed to end BBB meeting");
+ }
+ }
+
+ /**
+ * Test connection to BBB server and validate authentication
+ * Uses getMeetings which requires a valid checksum (shared secret)
+ * If auth fails, BBB returns FAILED with specific error message
+ */
+ async testConnection(): Promise {
+ try {
+ const url = this.buildApiUrl("getMeetings", {});
+ const response = await axios.get(url, {
+ timeout: 5000,
+ });
+
+ // Check response status and content
+ if (response.status !== 200) return false;
+
+ // BBB returns FAILED for auth errors, SUCCESS for valid auth (even empty meetings)
+ if (response.data.includes("FAILED")) {
+ return false;
+ }
+
+ // Valid auth should include returncode SUCCESS or valid XML structure
+ return response.data.includes("SUCCESS") ||
+ response.data.includes("");
+ } catch (error) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/lib/index.ts b/packages/app-store/bigbluebutton/lib/index.ts
new file mode 100644
index 00000000000000..fa4541d1fa4536
--- /dev/null
+++ b/packages/app-store/bigbluebutton/lib/index.ts
@@ -0,0 +1,2 @@
+export { default as VideoApiAdapter } from "./VideoApiAdapter";
+export { BBBApi } from "./bbb-api";
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/package.json b/packages/app-store/bigbluebutton/package.json
new file mode 100644
index 00000000000000..f63311634080db
--- /dev/null
+++ b/packages/app-store/bigbluebutton/package.json
@@ -0,0 +1,13 @@
+{
+ "private": true,
+ "name": "@calcom/bigbluebutton",
+ "version": "0.0.0",
+ "main": "./index.ts",
+ "description": "BigBlueButton is an open-source video conferencing system for education and business.",
+ "dependencies": {
+ "@calcom/lib": "workspace:*"
+ },
+ "devDependencies": {
+ "@calcom/types": "workspace:*"
+ }
+}
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/static/icon.svg b/packages/app-store/bigbluebutton/static/icon.svg
new file mode 100644
index 00000000000000..27c9a0fca63ecd
--- /dev/null
+++ b/packages/app-store/bigbluebutton/static/icon.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/packages/app-store/bigbluebutton/zod.ts b/packages/app-store/bigbluebutton/zod.ts
new file mode 100644
index 00000000000000..86b161dee9880f
--- /dev/null
+++ b/packages/app-store/bigbluebutton/zod.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const appKeysSchema = z.object({
+ serverUrl: z.string().url("Please provide a valid server URL"),
+ sharedSecret: z.string().trim().min(8, "Shared secret must be at least 8 characters").refine(
+ (value) => value.trim() === value && value.length > 0,
+ "Shared secret cannot be empty or contain only whitespace"
+ ),
+});
+
+export const appDataSchema = z.object({});
\ No newline at end of file
diff --git a/packages/app-store/video.adapters.generated.ts b/packages/app-store/video.adapters.generated.ts
index f1e0f05ffe7f78..74252cdcac0a37 100644
--- a/packages/app-store/video.adapters.generated.ts
+++ b/packages/app-store/video.adapters.generated.ts
@@ -10,6 +10,7 @@ export const VideoApiAdapterMap =
huddle01video: import("./huddle01video/lib/VideoApiAdapter"),
jelly: import("./jelly/lib/VideoApiAdapter"),
jitsivideo: import("./jitsivideo/lib/VideoApiAdapter"),
+ bigbluebutton: import("./bigbluebutton/lib/VideoApiAdapter"),
lyra: import("./lyra/lib/VideoApiAdapter"),
nextcloudtalk: import("./nextcloudtalk/lib/VideoApiAdapter"),
office365video: import("./office365video/lib/VideoApiAdapter"),