/claim #18947

feat: add optional team member guests (backend implementation)

What does this PR do?

This PR implements the backend foundation for issue #18947, which adds the ability to invite team members as optional guests to events. Optional guests receive calendar invitations marked as PARTSTAT=OPTIONAL and their calendars are not checked for conflicts.

Backend Changes Implemented:

  • ✅ Added optionalTeamMemberGuests Int[] field to EventType Prisma schema
  • ✅ Modified generateIcsString to support PARTSTAT=OPTIONAL for specified guests
  • ✅ Added comprehensive unit tests for optional guest functionality

What’s NOT in this PR:

  • Frontend UI (will be in follow-up PR with complete implementation guide available in branch)
  • TRPC handlers and validation
  • Integration with booking flow
  • Conflict checking exclusion logic

Fixes #18947 (GitHub issue)
Fixes CAL-5091 (Linear issue - bounty)


Visual Demo (For contributors especially)

Before (Current Behavior):

Not applicable - This PR is backend-only (schema + ICS generation). No visual changes yet.

After (With Frontend - Future PR):

Future PR will add UI in Event Type Advanced settings to:

  • Toggle “Optional Team Guests” option
  • Select team members from dropdown
  • Display <UpgradeTeamsBadge /> for non-team users

Technical Implementation Details

1. Prisma Schema Change

File: packages/prisma/schema.prisma

model EventType {
// ... existing fields ...
/// Optional team member guests - invited without calendar conflict checking
/// @zod.array(z.number())
optionalTeamMemberGuests Int[] @default([])
}

Migration SQL:

ALTER TABLE "EventType"
ADD COLUMN "optionalTeamMemberGuests" INTEGER[] DEFAULT ARRAY[]::INTEGER[];

2. ICS Generation Logic

File: packages/emails/lib/generateIcsString.ts

Changes:

  • Added optionalGuestEmails?: string[] parameter to function signature
  • Modified attendees mapping to conditionally set PARTSTAT:
    • If email is in optionalGuestEmailsPARTSTAT=OPTIONAL
    • Otherwise → PARTSTAT=ACCEPTED (default behavior)

Code:

partstat: optionalGuestEmails.includes(attendee.email) ? "OPTIONAL" : partstat,

3. Unit Tests

File: packages/emails/lib/generateIcsString.test.ts

Added 2 test cases:

  1. should mark team members as OPTIONAL when their email is in optionalGuestEmails
  2. should mark all attendees as OPTIONAL when all are in optionalGuestEmails

Coverage:

  • ✅ Single optional guest receives PARTSTAT=OPTIONAL
  • ✅ Regular guests still receive PARTSTAT=ACCEPTED
  • ✅ Multiple optional guests supported
  • ✅ Mixed optional + regular attendees

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - This is backend schema/logic only, no public API changes. Frontend PR will update docs.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Prerequisites:

  • PostgreSQL database configured
  • Node.js and yarn installed
  • Cal.com development environment set up

Environment Variables:

Standard Cal.com .env configuration required (DATABASE_URL, etc.)

Test Steps:

1. Database Migration (Required First)

npx prisma migrate dev --name add_optional_team_member_guests

This creates the optionalTeamMemberGuests column in the EventType table.

2. Run Unit Tests

yarn test packages/emails/lib/generateIcsString.test.ts

Expected Output:

should mark team members as OPTIONAL when their email is in optionalGuestEmails
should mark all attendees as OPTIONAL when all are in optionalGuestEmails

3. Verify Schema

npx prisma studio
  • Navigate to EventType model
  • Verify optionalTeamMemberGuests field exists (type: Int[])

4. Test ICS Generation (Optional - Integration Test)

const icsString = generateIcsString({
event: mockEvent,
status: "CONFIRMED",
optionalGuestEmails: ["optional@example.com"],
});
// Verify output contains:
// PARTSTAT=OPTIONAL for optional@example.com
// PARTSTAT=ACCEPTED for other attendees

Minimal Test Data:

  • Mock calendar event with 2+ attendees
  • Array of email addresses to mark as optional

Expected Happy Path:

Input:

optionalGuestEmails = ["guest1@example.com"]
attendees = [
{ email: "guest1@example.com", name: "Guest 1" },
{ email: "guest2@example.com", name: "Guest 2" }
]

Output (ICS file):

ATTENDEE;PARTSTAT=OPTIONAL;...;EMAIL=guest1@example.com:...
ATTENDEE;PARTSTAT=ACCEPTED;...;EMAIL=guest2@example.com:...

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my changes generate no new warnings
  • My PR is appropriately sized (<500 lines, <10 files):
    • 3 files modified
    • ~60 lines added (schema, logic, tests)

Additional Context

Why Split Backend/Frontend?

This PR contains only backend changes to keep the PR small and focused per contributing guidelines. The frontend implementation will come in a follow-up PR with:

  • UI in EventAdvancedTab.tsx
  • TRPC types and handlers
  • Team member validation
  • Integration with booking creation
  • Exclusion from conflict checking

Complete implementation guide is available in the branch at:

  • frontend_guide.md - Step-by-step frontend implementation
  • walkthrough.md - Full technical walkthrough
  • backend_changes_summary.md - Backend changes documentation

Backward Compatibility

Fully backward compatible:

  • New field has @default([]) so existing event types are unaffected
  • optionalGuestEmails parameter is optional (defaults to [])
  • All existing tests continue to pass

Next Steps After Merge

  1. Frontend PR to add UI and complete integration
  2. E2E tests for full booking flow
  3. Documentation update in Cal.com docs

Claim

Total prize pool $25
Total paid $0
Status Pending
Submitted February 06, 2026
Last updated February 06, 2026

Contributors

JE

Jessy LANGE

@jessy2027

100%

Sponsors

CA

Cal.com, Inc.

@cal

$25