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:
- Show recurrence badge on meeting cards (RotateIcon + "Weekly" label)
- Expand recurring meetings across calendar days
- 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
Recommended Backend Implementation
Option A: Expand on Create (Recommended)
When CREATE_MEETING is called with metadata.recurrence, the backend creates individual meeting records for each occurrence:
- Parse
recurrence.type,recurrence.days, and end condition - Generate all occurrence dates:
- DAILY: Every day from
scheduledAtuntil end - WEEKLY with
days: [1,3,5]: Every Mon/Wed/Fri fromscheduledAtweek - BIWEEKLY with
days: [1,3,5]: Every other week's Mon/Wed/Fri - MONTHLY: Same day-of-month each month
- DAILY: Every day from
- Create a meeting record for each date with:
- Same title, type, duration, timezone, attendees
- Unique
scheduledAtper occurrence - Link to parent via
recurrenceGroupId(new field) - Store
recurrencemetadata 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:
myMeetingsaccepts 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
| Index | Day |
|---|---|
| 0 | Sunday |
| 1 | Monday |
| 2 | Tuesday |
| 3 | Wednesday |
| 4 | Thursday |
| 5 | Friday |
| 6 | Saturday |
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
| Priority | Change | Effort | Impact |
|---|---|---|---|
| P1 | Return metadata in myMeetings and meeting queries | Low | Unblocks calendar recurrence display and update modal pre-fill |
| P2 | Accept metadata in UpdateMeetingInput | Low | Allows editing recurrence on existing meetings |
| P3 | Server-side recurrence expansion (create individual occurrences) | High | Real per-occurrence meetings with status, insights, invites |
| P4 | Date-range filtering on ListMeetingsInput | Medium | Efficient calendar queries at scale |
| P5 | Recurrence-aware update/cancel scopes | Medium | Google 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:
- Add
metadatatoGET_MY_MEETINGSquery ingraphql/operations/interview/queries.ts - Add
metadatatoGET_MEETING_BY_IDquery ingraphql/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.