Calendar events that arrive without a stable UID create duplicate entries on every resend — a customer's calendar fills with phantom copies of the same appointment every time the booking system regenerates an invite. RFC 5545 mandates a persistent, globally unique UID for each VEVENT precisely to enable clients to match updates to existing events rather than creating new ones. Missing mandatory fields (ORGANIZER, DTEND, ATTENDEE) causes some calendar clients to silently reject the invite. A UID generated with uuidv4() on each call guarantees that every invite opens as a new event, making reschedules and cancellations invisible to the customer's calendar app.
High because a randomly generated or absent UID causes calendar clients to treat every invite as a new event, making updates and cancellations invisible and filling customer calendars with duplicate entries.
Use the ics library in src/lib/calendar.ts and derive the UID from the booking ID — never generate it with uuidv4() inside the generation function. Include all 6 mandatory VEVENT fields plus ATTENDEE, ORGANIZER, LOCATION, and METHOD:
import { createEvent } from 'ics'
export async function generateBookingICS(booking: Booking, host: User) {
const event = {
uid: `${booking.id}@yourdomain.com`, // Stable — never random
title: `Appointment: ${booking.service_name}`,
start: parseICSDate(booking.start_time),
end: parseICSDate(booking.end_time),
description: booking.notes ?? '',
organizer: { name: host.name, email: host.email },
attendees: [{ name: booking.customer_name, email: booking.customer_email, rsvp: true }],
location: booking.location ?? '',
method: 'PUBLISH',
}
return createEvent(event)
}
ID: booking-notifications-reminders.calendar-invites.ics-complete-uid
Severity: high
What to look for: Find the .ics file generation code — search for ics library imports (ical.js, node-ical, createEvent) or manual VCALENDAR string construction. Enumerate the following 10 VEVENT fields: (1) UID, (2) DTSTAMP, (3) DTSTART, (4) DTEND, (5) SUMMARY, (6) DESCRIPTION, (7) ORGANIZER, (8) ATTENDEE, (9) LOCATION, (10) METHOD. For each field, check if it is present in the generated output. For UID specifically: verify the UID is derived from the booking ID (e.g., booking.id@domain.com) and is NOT regenerated on each call. Check if UID generation uses a stable input (booking ID) versus a random value (uuid() called each time).
Pass criteria: Count all 10 VEVENT fields. ALL of the following: (1) At least 8 of 10 VEVENT fields are present — enumerate each field found and each missing, report the count even on pass (e.g., "9 of 10 fields present"). All 6 minimum required fields (UID, DTSTAMP, DTSTART, DTEND, SUMMARY, ORGANIZER) must be present. (2) UID is derived from a stable booking identifier (quote the UID generation code) and does NOT change across calls for the same booking. A randomly generated UID (e.g., uuidv4() called each time) does NOT count as stable.
Fail criteria: Fewer than 8 of 10 fields present. Any of the 6 minimum required fields missing. UID is generated randomly on each invocation (e.g., uuidv4() called inside the generation function without using booking ID). UID changes when the same booking is processed again.
Skip (N/A) when: No calendar invite generation code found — no ics/ical library in dependencies AND no manual VCALENDAR string construction in codebase (searched for "BEGIN:VCALENDAR", "VEVENT", "createEvent", "ical" imports).
Detail on fail: Enumerate all 10 fields: "FOUND" or "MISSING" for each. Quote the UID generation code. Example: "Found 6/10 fields: UID (FOUND, stable: booking.id@domain), DTSTAMP (FOUND), DTSTART (FOUND), DTEND (FOUND), SUMMARY (FOUND), DESCRIPTION (FOUND). MISSING: ORGANIZER, ATTENDEE, LOCATION, METHOD.".
Remediation: Generate .ics with all required fields and a stable UID based on booking ID:
// src/lib/calendar.ts
import { createEvent } from 'ics'
export async function generateBookingICS(booking: Booking, host: User) {
// UID MUST be stable — derived from booking.id, never random
const event = {
title: `Appointment: ${booking.service_name}`,
description: booking.notes || '',
start: parseICSDate(booking.start_time),
end: parseICSDate(booking.end_time),
uid: `${booking.id}@yourdomain.com`, // Stable across calls
organizer: { name: host.name, email: host.email },
attendees: [{ name: booking.customer_name, email: booking.customer_email, rsvp: true }],
location: booking.location || '',
method: 'PUBLISH',
}
return createEvent(event)
}
Cross-reference: Booking Flow & Lifecycle audit checks booking ID stability. Email/SMS Compliance audit checks email attachment handling. Booking Calendar Availability audit checks time/date handling.