Skip to main content

Meetings: Recurring Meetings — Backend Requirements

Summary

The frontend now supports creating and editing recurring meetings with day-of-week selection. Currently, recurrence data is stored in the meeting's metadata JSON field, and the calendar expands occurrences client-side. However, several backend changes are needed to make this fully functional.

This document outlines what the backend needs to implement, in priority order.


Current State (Frontend)

What the frontend sends on CREATE

The CreateMeetingInput already accepts a metadata JSON field. The frontend sends recurrence config inside it:

{
"title": "Weekly Standup",
"type": "GENERAL",
"companyId": "...",
"scheduledAt": "2026-03-13T09:00:00.000Z",
"duration": 30,
"timezone": "Europe/Stockholm",
"metadata": {
"createdFrom": "frontend",
"createdAt": "2026-03-13T...",
"recurrence": {
"type": "WEEKLY",
"days": [1, 3, 5],
"endType": "OCCURRENCES",
"occurrences": 10
}
}
}

What the frontend sends on UPDATE

The frontend now sends metadata in UpdateMeetingInput (including updated recurrence). The backend must accept metadata in UpdateMeetingInput — see Priority 1 below.

Recurrence metadata shape

interface RecurrenceMetadata {
type: "NONE" | "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY"
days?: number[] // Weekday indices: 0=Sun, 1=Mon, ..., 6=Sat (WEEKLY/BIWEEKLY only)
endType: "OCCURRENCES" | "UNTIL"
occurrences?: number // When endType = "OCCURRENCES" (1-365)
endDate?: string // ISO date string when endType = "UNTIL"
}

What the frontend needs from queries

The frontend reads metadata to:

  1. Show recurrence badge on meeting cards (RotateIcon + "Weekly" label)
  2. Expand recurring meetings across calendar days
  3. Pre-fill recurrence fields when editing a meeting

Priority 1: Return metadata in queries (BLOCKING)

Problem

Neither GET_MY_MEETINGS nor GET_MEETING_BY_ID currently return the metadata field. Without it:

  • The calendar view cannot expand recurring meetings across days
  • Meeting cards cannot show the recurrence indicator (e.g., "Weekly")
  • The Update Meeting modal cannot pre-fill recurrence fields

Required Changes

1. Add metadata to the Meeting type resolver (if not already there):

type Meeting {
# ... existing fields ...
metadata: JSON # Already exists on CREATE, just needs to be queryable
}

2. Return metadata in myMeetings query (list query):

The frontend query will be updated to:

query GetMyMeetings($input: ListMeetingsInput!) {
myMeetings(input: $input) {
id
interviewBookingId
meetingUrl
roomName
status
scheduledAt
duration
title
metadata # <-- ADD THIS
}
}

3. Return metadata in meeting query (detail query):

The frontend query will be updated to:

query GetMeetingById($id: ID!) {
meeting(id: $id) {
# ... existing fields ...
metadata # <-- ADD THIS
}
}

4. Return metadata in updateMeeting mutation response (already added on frontend):

mutation UpdateMeeting($input: UpdateMeetingInput!) {
updateMeeting(input: $input) {
id
title
description
scheduledAt
duration
status
metadata # <-- ADD THIS
}
}

Priority 2: Accept metadata in UpdateMeetingInput

Problem

The UpdateMeetingInput likely only accepts id, title, description, scheduledAt, duration. The frontend now sends metadata when updating a meeting (to persist recurrence changes). If the backend ignores it, editing recurrence in an existing meeting has no effect.

Required Change

input UpdateMeetingInput {
id: ID!
title: String
description: String
scheduledAt: DateTime
duration: Int
metadata: JSON # <-- ADD THIS
}

The resolver should merge incoming metadata with existing metadata (not replace entirely), or the frontend will send the full object.


Priority 3 (Future): Server-Side Recurrence Expansion

Currently the frontend expands recurring meetings client-side on the calendar. This has limitations:

  • Only works visually in the calendar — no real meeting instances exist for each occurrence
  • Cannot send reminders or calendar invites for individual occurrences
  • Cannot mark individual occurrences as completed/cancelled
  • No proper date-range query filtering — the frontend fetches all meetings and expands locally

When CREATE_MEETING is called with metadata.recurrence, the backend creates individual meeting records for each occurrence:

  1. Parse recurrence.type, recurrence.days, and end condition
  2. Generate all occurrence dates:
    • DAILY: Every day from scheduledAt until end
    • WEEKLY with days: [1,3,5]: Every Mon/Wed/Fri from scheduledAt week
    • BIWEEKLY with days: [1,3,5]: Every other week's Mon/Wed/Fri
    • MONTHLY: Same day-of-month each month
  3. Create a meeting record for each date with:
    • Same title, type, duration, timezone, attendees
    • Unique scheduledAt per occurrence
    • Link to parent via recurrenceGroupId (new field)
    • Store recurrence metadata on all instances for reference
type Meeting {
# ... existing fields ...
recurrenceGroupId: ID # Links all occurrences together
recurrenceIndex: Int # 0-based occurrence index
isRecurring: Boolean # Quick check without parsing metadata
}

Benefits:

  • Each occurrence is a real meeting with its own status, attendees, insights
  • Standard date-range queries work for filtering
  • Can cancel/reschedule individual occurrences
  • Calendar invites work per occurrence

Option B: Virtual Expansion on Query

Instead of creating records, the backend expands recurring meetings at query time:

  • myMeetings accepts date range (startDate, endDate)
  • For each recurring meeting, generate virtual instances within the range
  • Return them as regular meeting objects with a recurrenceGroupId

This is lighter on storage but harder to manage per-occurrence state.

Recurrence Algorithm (for Option A)

function expandRecurrence(meeting, recurrence):
dates = []
cursor = meeting.scheduledAt

if recurrence.type == "DAILY":
while not pastEnd(cursor, recurrence):
dates.push(cursor)
cursor += 1 day

if recurrence.type == "WEEKLY":
if recurrence.days exists:
weekStart = startOfWeek(cursor)
while not pastEnd(weekStart, recurrence):
for day in recurrence.days:
d = weekStart + day days
if d >= meeting.scheduledAt and not pastEnd(d, recurrence):
dates.push(d)
weekStart += 7 days
else:
while not pastEnd(cursor, recurrence):
dates.push(cursor)
cursor += 7 days

if recurrence.type == "BIWEEKLY":
// Same as WEEKLY but weekStart += 14 days

if recurrence.type == "MONTHLY":
while not pastEnd(cursor, recurrence):
dates.push(cursor)
cursor += 1 month (same day, clamped to month end)

return dates

function pastEnd(date, recurrence):
if recurrence.endType == "UNTIL":
return date > recurrence.endDate
if recurrence.endType == "OCCURRENCES":
return count >= recurrence.occurrences

Day-of-Week Index Reference

IndexDay
0Sunday
1Monday
2Tuesday
3Wednesday
4Thursday
5Friday
6Saturday

This follows JavaScript's Date.getDay() convention.


Priority 4 (Future): Date-Range Filtering for Calendar

Currently GET_MY_MEETINGS fetches all meetings with a simple limit. For the calendar to work efficiently at scale, the backend should support date-range filtering:

Proposed Input Change

input ListMeetingsInput {
limit: Int
includeAttendedMeetings: Boolean
startDate: DateTime # <-- ADD
endDate: DateTime # <-- ADD
status: MeetingStatus # <-- ADD (optional, filtering server-side)
}

This allows the frontend to fetch only meetings visible in the current calendar month, instead of all meetings.


Priority 5 (Future): Recurrence-Aware Mutations

If the backend implements server-side recurrence (Priority 3), these mutations should be considered:

Update Scope

When editing a recurring meeting, ask the user: "This meeting" | "This and future" | "All meetings":

input UpdateMeetingInput {
id: ID!
title: String
description: String
scheduledAt: DateTime
duration: Int
metadata: JSON
updateScope: RecurrenceUpdateScope # THIS_ONLY | THIS_AND_FUTURE | ALL
}

enum RecurrenceUpdateScope {
THIS_ONLY
THIS_AND_FUTURE
ALL
}

Cancel Scope

Same pattern for cancellation:

input CancelMeetingInput {
meetingId: ID!
reason: String
cancelScope: RecurrenceUpdateScope # THIS_ONLY | THIS_AND_FUTURE | ALL
}

Delete Recurrence Series

mutation DeleteRecurrenceSeries($recurrenceGroupId: ID!) {
deleteRecurrenceSeries(recurrenceGroupId: $recurrenceGroupId): Boolean
}

Summary Table

PriorityChangeEffortImpact
P1Return metadata in myMeetings and meeting queriesLowUnblocks calendar recurrence display and update modal pre-fill
P2Accept metadata in UpdateMeetingInputLowAllows editing recurrence on existing meetings
P3Server-side recurrence expansion (create individual occurrences)HighReal per-occurrence meetings with status, insights, invites
P4Date-range filtering on ListMeetingsInputMediumEfficient calendar queries at scale
P5Recurrence-aware update/cancel scopesMediumGoogle Calendar-like "this/future/all" editing

Frontend Changes Needed After Backend P1

Once the backend returns metadata in queries, the frontend needs these small updates:

  1. Add metadata to GET_MY_MEETINGS query in graphql/operations/interview/queries.ts
  2. Add metadata to GET_MEETING_BY_ID query in graphql/operations/interview/queries.ts

These are one-line additions to the existing GQL queries. The rest of the frontend (calendar expansion, recurrence badges, UpdateMeetingModal pre-fill) is already built and waiting for the data.