Skip to content

feat(app-store): add BigBlueButton video conferencing integration#29453

Open
ThaiTrevor wants to merge 2 commits into
calcom:mainfrom
ThaiTrevor:fix/issue-1985-calcom-cal-3105-bigbluebutton-integration
Open

feat(app-store): add BigBlueButton video conferencing integration#29453
ThaiTrevor wants to merge 2 commits into
calcom:mainfrom
ThaiTrevor:fix/issue-1985-calcom-cal-3105-bigbluebutton-integration

Conversation

@ThaiTrevor
Copy link
Copy Markdown

🤖 AI-assisted contribution — This PR was drafted with AI assistance and reviewed by a native Vietnamese speaker before submission. Placeholders, terminology, and file format were validated automatically. I will respond to review feedback. Happy to revise or close if not a fit.


Summary

Adds a BigBlueButton conferencing app to the app-store, addressing the
long-standing request in #1985 / CAL-3105.

The integration follows the existing jitsivideo pattern (dynamic location,
shared-secret auth — no OAuth) and exposes a real VideoApiAdapter that
actually provisions meetings on the BBB server, rather than only producing
a static join URL:

  • New app at packages/app-store/bigbluebutton/ with _metadata.ts,
    package.json, index.ts, zod.ts, api/add.ts, lib/VideoApiAdapter.ts,
    lib/bbb.ts, lib/getBigBlueButtonAppKeys.ts, static/icon.svg, and
    DESCRIPTION.md.
  • Admin-configurable app keys (bbb_url, bbb_secret) following the same
    pattern as Daily / Jitsi.
  • lib/bbb.ts implements the standard BBB SHA-1 shared-secret checksum
    (<callName><queryString><sharedSecret>) per the BBB API security model.
  • VideoApiAdapter.createMeeting() calls create then returns a signed
    join URL (redirect=true). deleteMeeting() calls end.
  • Registered the new app in all of the autogenerated app-store registries:
    apps.metadata.generated.ts, apps.server.generated.ts,
    video.adapters.generated.ts, apps.keys-schemas.generated.ts,
    apps.schemas.generated.ts, bookerApps.metadata.generated.ts.
  • Added the cal_provide_bigbluebutton_meeting_url locale string.

Closes #1985

Test plan

  • Added unit tests in packages/app-store/bigbluebutton/lib/bbb.test.ts
    covering buildQueryString, computeChecksum, and buildApiUrl
    (including the trailing-slash and no-params edge cases).
  • Manually verified the checksum logic against an ad-hoc Node script:
    computeChecksum('create', 'name=Test&meetingID=abc', 'supersecret')
    matches sha1('createname=Test&meetingID=abcsupersecret').
  • Maintainer to verify in CI: yarn test packages/app-store/bigbluebutton
    and yarn type-check.
  • Maintainer to install the app from /apps, set BBB server URL and
    shared secret in /settings/admin/apps/bigbluebutton, and create a
    booking using the BigBlueButton location.

Notes

  • Used the jitsivideo adapter as the reference template; this is the
    closest existing analogue (shared-secret auth, dynamic link, no OAuth).
  • The video adapter is registered under the key bigbluebutton and
    getVideoAdapters.ts already strips _video from credential types, so
    the existing lookup fallback resolves bigbluebutton_video correctly.
  • No new env vars are introduced — server URL and shared secret live in
    the standard app.keys admin-settings flow per CONTRIBUTING.md.
  • A placeholder icon.svg is included. The maintainer may want to swap it
    for the official BigBlueButton wordmark before merging.

ThaiTrevor and others added 2 commits May 26, 2026 11:39
Adds a new conferencing app for BigBlueButton (closes calcom#1985 / CAL-3105).

The integration follows the jitsivideo pattern with a server-side
VideoApiAdapter that authenticates BBB API requests using the standard
SHA-1 shared-secret checksum, so meetings are actually created on the
BBB server (rather than just generating a static link).

Admin configures the BBB server URL and shared secret via the standard
app-keys flow (packages/app-store/bigbluebutton/zod.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @ThaiTrevor! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added $50 app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar community Created by Linear-GitHub Sync consumer ✨ feature New feature or request 💎 Bounty A bounty on Algora.io 🙋🏻‍♂️help wanted Help from the community is appreciated labels May 26, 2026
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@ThaiTrevor ThaiTrevor marked this pull request as ready for review May 27, 2026 03:02
@ThaiTrevor
Copy link
Copy Markdown
Author

Hi maintainers! Could one of you please add the run-ci label so the CI checks can run? Happy to address any review feedback after CI passes. Thanks!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds a complete BigBlueButton video conferencing integration to the Cal.com app store. The implementation includes cryptographic utilities for signed API requests, Zod schemas for configuration validation, a VideoApiAdapter for managing meeting lifecycle (creation, deletion, metadata resolution), a Next.js API handler for credential installation with team authorization, and full registry wiring into the app-store system. The package is self-contained with internal exports and external dependencies on @calcom/lib, @calcom/prisma, and @calcom/types.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely describes the main feature being added: BigBlueButton video conferencing integration to the app-store.
Description check ✅ Passed The pull request description thoroughly explains the changes, implementation approach, testing, and references the linked issue #1985, directly addressing the feature request.
Linked Issues check ✅ Passed The PR successfully implements the BigBlueButton API integration requested in issue #1985, including app registration, admin-configurable keys, checksum-based authentication, and a VideoApiAdapter for meeting provisioning.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the BigBlueButton integration as specified in issue #1985; no out-of-scope modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app-store/bigbluebutton/api/add.ts`:
- Around line 18-26: Validate the teamId from req.query before coercing to
Number: check that teamId is a single string and matches a numeric pattern (or
use Number.isInteger after parsing) and if invalid respond with
res.status(400).send(...) before calling throwIfNotHaveAdminAccessToTeam or
building installForObject; when valid, convert once to a Number (e.g., const
parsedTeamId = Number(teamId)) and pass parsedTeamId to
throwIfNotHaveAdminAccessToTeam and to installForObject (or use parsedTeamId ? {
teamId: parsedTeamId } : { userId: req.session.user.id }) to avoid NaN or array
inputs.
- Around line 28-44: The current findFirst → create flow in add.ts
(prisma.credential.findFirst / prisma.credential.create) is race-prone; make
installation idempotent by enforcing uniqueness at the DB boundary: add a
composite unique constraint to the Credential model in
packages/prisma/schema.prisma (e.g. @@unique([appId, type, userId, teamId]) or
an appropriate subset that represents a single install identity), run a
migration, and then update add.ts to use a conflict-safe operation (replace the
findFirst/create pair with a single upsert or keep create wrapped with handling
for unique-violation errors) so concurrent requests cannot create duplicate
credentials.

In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts`:
- Line 79: The log at VideoApiAdapter (in the method handling the BigBlueButton
create response) currently prints the entire remote response body; change it to
avoid logging raw body content by parsing the response safely and logging only
normalized error fields such as response.status, response.statusCode (if
present), and any explicit error/code/message fields (e.g., body.code or
body.message), or a short sanitized summary (first N chars) if structured fields
are absent; update the log.error call to include only those extracted fields and
context (e.g., "BBB create failed" + status/code/message) instead of the raw
body string.
- Around line 75-80: Replace the two plain throws in VideoApiAdapter.ts with
ErrorWithCode instances: when the fetch/create fails (the "Unable to create
BigBlueButton meeting" throw) throw new ErrorWithCode with a suitable code
(e.g., "EXTERNAL_SERVICE_ERROR" or "UNAVAILABLE") and the same message; when the
response body indicates failure (the "BigBlueButton rejected the create request"
case) throw ErrorWithCode including a descriptive code (e.g., "BAD_RESPONSE" or
"EXTERNAL_SERVICE_ERROR") and include the response body in the error message or
metadata for diagnostics; import ErrorWithCode from your project's error
utilities and keep the existing log.error call intact.
- Around line 103-107: The deleteMeeting implementation is calling
buildApiUrl("end", ...) with password: "" which will fail to end meetings;
update deleteMeeting to pass the moderator password into buildApiUrl (either by
adding a moderatorPassword parameter to deleteMeeting or by retrieving the
stored moderator/moderatorPW for meetingID from wherever meetings are persisted)
and use that value instead of an empty string when constructing endUrl; ensure
references to buildApiUrl, deleteMeeting, meetingID and password are updated so
the real moderator password is supplied to the BBB end call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 91aa76c9-5cf2-4c15-850f-2aeb3cf337ee

📥 Commits

Reviewing files that changed from the base of the PR and between 180ede2 and 54f2f43.

⛔ Files ignored due to path filters (1)
  • packages/app-store/bigbluebutton/static/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (20)
  • .bounty_pr.json
  • packages/app-store/apps.keys-schemas.generated.ts
  • packages/app-store/apps.metadata.generated.ts
  • packages/app-store/apps.schemas.generated.ts
  • packages/app-store/apps.server.generated.ts
  • packages/app-store/bigbluebutton/DESCRIPTION.md
  • packages/app-store/bigbluebutton/_metadata.ts
  • packages/app-store/bigbluebutton/api/add.ts
  • packages/app-store/bigbluebutton/api/index.ts
  • packages/app-store/bigbluebutton/index.ts
  • packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts
  • packages/app-store/bigbluebutton/lib/bbb.test.ts
  • packages/app-store/bigbluebutton/lib/bbb.ts
  • packages/app-store/bigbluebutton/lib/getBigBlueButtonAppKeys.ts
  • packages/app-store/bigbluebutton/lib/index.ts
  • packages/app-store/bigbluebutton/package.json
  • packages/app-store/bigbluebutton/zod.ts
  • packages/app-store/bookerApps.metadata.generated.ts
  • packages/app-store/video.adapters.generated.ts
  • packages/i18n/locales/en/common.json

Comment on lines +18 to +26
const { teamId, returnTo } = req.query;

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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate teamId before numeric coercion.

Number(teamId) can become NaN (e.g., array/invalid input). Reject invalid teamId with 400 before auth/query logic to avoid ambiguous behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/bigbluebutton/api/add.ts` around lines 18 - 26, Validate
the teamId from req.query before coercing to Number: check that teamId is a
single string and matches a numeric pattern (or use Number.isInteger after
parsing) and if invalid respond with res.status(400).send(...) before calling
throwIfNotHaveAdminAccessToTeam or building installForObject; when valid,
convert once to a Number (e.g., const parsedTeamId = Number(teamId)) and pass
parsedTeamId to throwIfNotHaveAdminAccessToTeam and to installForObject (or use
parsedTeamId ? { teamId: parsedTeamId } : { userId: req.session.user.id }) to
avoid NaN or array inputs.

Comment on lines +28 to +44
const alreadyInstalled = await prisma.credential.findFirst({
where: {
type: appType,
...installForObject,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
type: appType,
key: {},
...installForObject,
appId: "bigbluebutton",
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether Credential has a uniqueness constraint covering install identity.
# Expected: a unique index that prevents duplicate bigbluebutton installs per owner scope.

fd -i "schema.prisma" | head -n 5
rg -n -C4 'model\s+Credential|@@unique|\bunique\b|appId|type|teamId|userId' $(fd -i "schema.prisma")

Repository: calcom/cal.diy

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and print the full `model Credential` blocks from schema.prisma files in-repo.
for f in $(fd -i "schema.prisma"); do
  if rg -n "^[[:space:]]*model Credential[[:space:]]*{" "$f" >/dev/null; then
    echo "===== $f ====="
    start=$(rg -n "^[[:space:]]*model Credential[[:space:]]*{" "$f" | head -n1 | cut -d: -f1)
    awk -v s="$start" '
      NR>=s {
        print
        if (NR>s && $0 ~ /^[[:space:]]*}[[:space:]]*$/) { exit }
      }
    ' "$f"
    echo
  fi
done

Repository: calcom/cal.diy

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in $(fd -i "schema.prisma"); do
  # Locate the start line of `model Credential {` without regex braces issues
  start=$(rg -n -F "model Credential {" "$f" | head -n1 | cut -d: -f1 || true)
  if [ -n "${start:-}" ]; then
    echo "===== $f (model Credential starts at line $start) ====="
    awk -v s="$start" '
      NR>=s {
        print
        # Heuristic: stop at the first standalone closing brace after the block starts
        if (NR>s && $0 ~ /^[[:space:]]*}[[:space:]]*$/) exit
      }
    ' "$f"
    echo
  fi
done

Repository: calcom/cal.diy

Length of output: 1431


Make installation idempotent at the DB boundary.

packages/prisma/schema.prisma’s model Credential has only @@index(...) constraints (no @@unique(...)) for the install identity fields (appId, type, userId/teamId). That makes the findFirstcreate flow in packages/app-store/bigbluebutton/api/add.ts race-prone, so concurrent requests can create duplicate credentials. Add a DB-enforced unique constraint (or conflict-safe upsert/unique-violation handling) to make installation idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/bigbluebutton/api/add.ts` around lines 28 - 44, The
current findFirst → create flow in add.ts (prisma.credential.findFirst /
prisma.credential.create) is race-prone; make installation idempotent by
enforcing uniqueness at the DB boundary: add a composite unique constraint to
the Credential model in packages/prisma/schema.prisma (e.g. @@unique([appId,
type, userId, teamId]) or an appropriate subset that represents a single install
identity), run a migration, and then update add.ts to use a conflict-safe
operation (replace the findFirst/create pair with a single upsert or keep create
wrapped with handling for unique-violation errors) so concurrent requests cannot
create duplicate credentials.

Comment on lines +75 to +80
throw new Error("Unable to create BigBlueButton meeting");
}
const body = await response.text();
if (!/<returncode>SUCCESS<\/returncode>/i.test(body)) {
log.error(`BigBlueButton create call returned a failure: ${body}`);
throw new Error("BigBlueButton rejected the create request");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use ErrorWithCode instead of generic Error in this adapter.

Lines 75 and 80 throw plain Error in a non-tRPC file; this violates the project error-handling convention and reduces structured handling upstream.

As per coding guidelines: "Use ErrorWithCode for errors in non-tRPC files (services, repositories, utilities); use TRPCError only in tRPC routers".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` around lines 75 -
80, Replace the two plain throws in VideoApiAdapter.ts with ErrorWithCode
instances: when the fetch/create fails (the "Unable to create BigBlueButton
meeting" throw) throw new ErrorWithCode with a suitable code (e.g.,
"EXTERNAL_SERVICE_ERROR" or "UNAVAILABLE") and the same message; when the
response body indicates failure (the "BigBlueButton rejected the create request"
case) throw ErrorWithCode including a descriptive code (e.g., "BAD_RESPONSE" or
"EXTERNAL_SERVICE_ERROR") and include the response body in the error message or
metadata for diagnostics; import ErrorWithCode from your project's error
utilities and keep the existing log.error call intact.

}
const body = await response.text();
if (!/<returncode>SUCCESS<\/returncode>/i.test(body)) {
log.error(`BigBlueButton create call returned a failure: ${body}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging full BBB response body on failures.

Line 79 logs raw remote body, which can include meeting metadata/user-provided text. Prefer logging normalized error fields/status only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` at line 79, The log
at VideoApiAdapter (in the method handling the BigBlueButton create response)
currently prints the entire remote response body; change it to avoid logging raw
body content by parsing the response safely and logging only normalized error
fields such as response.status, response.statusCode (if present), and any
explicit error/code/message fields (e.g., body.code or body.message), or a short
sanitized summary (first N chars) if structured fields are absent; update the
log.error call to include only those extracted fields and context (e.g., "BBB
create failed" + status/code/message) instead of the raw body string.

Comment on lines +103 to +107
const endUrl = buildApiUrl(
{ url: bbb_url, secret: bbb_secret },
"end",
{ meetingID, password: "" }
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

deleteMeeting is calling BBB end without a moderator password.

At Line 106, password: "" makes the end request very likely invalid for BBB, so meetings may never actually terminate.

Suggested fix
-      const endUrl = buildApiUrl(
-        { url: bbb_url, secret: bbb_secret },
-        "end",
-        { meetingID, password: "" }
-      );
+      const endUrl = buildApiUrl(
+        { url: bbb_url, secret: bbb_secret },
+        "end",
+        { meetingID, password: /* moderator password for this meeting */ }
+      );

This likely needs plumbing the moderator password into deleteMeeting (or persisting it where teardown can retrieve it).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` around lines 103 -
107, The deleteMeeting implementation is calling buildApiUrl("end", ...) with
password: "" which will fail to end meetings; update deleteMeeting to pass the
moderator password into buildApiUrl (either by adding a moderatorPassword
parameter to deleteMeeting or by retrieving the stored moderator/moderatorPW for
meetingID from wherever meetings are persisted) and use that value instead of an
empty string when constructing endUrl; ensure references to buildApiUrl,
deleteMeeting, meetingID and password are updated so the real moderator password
is supplied to the BBB end call.

@ThaiTrevor
Copy link
Copy Markdown
Author

@CLAassistant recheck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar 💎 Bounty A bounty on Algora.io community Created by Linear-GitHub Sync consumer ✨ feature New feature or request 🙋🏻‍♂️help wanted Help from the community is appreciated size/L $50

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CAL-3105] BigBlueButton Integration

2 participants