📆 Flexible Patterns
Daily, weekly, monthly, yearly frequencies with custom intervals. Repeat every 2 weeks or every 3 months.
Recurring events enable you to create events that repeat on a schedule - daily, weekly, monthly, or yearly. The system supports flexible recurrence patterns with configurable end conditions.
Recurring events provide:
📆 Flexible Patterns
Daily, weekly, monthly, yearly frequencies with custom intervals. Repeat every 2 weeks or every 3 months.
🛑 End Conditions
Choose how recurrence ends: never, specific date, or after N occurrences (max 100).
🔄 Instance Generation
Parent event defines pattern, instances are generated automatically for each occurrence.
🗑️ Cascade Delete
Deleting parent event automatically removes all generated instances.
import { getDatabase } from '#libs/database'import type { EventData } from '#libs/database'
const db = getDatabase()
// Weekly meeting for 12 weeksconst recurringEvent: EventData = { name: 'Weekly Team Sync', event_date: '2025-12-10', start_time: '14:00', duration: 60, location: 'Conference Room A', created_by: userId,
// Recurrence configuration is_recurring: true, recurrence_frequency: 'weekly', recurrence_interval: 1, // Every week recurrence_end_type: 'count', recurrence_end_count: 12, // 12 occurrences}
const eventId = await db.insertEvent(recurringEvent)Repeat every N days
{ is_recurring: true, recurrence_frequency: 'daily', recurrence_interval: 1, // Every day recurrence_end_type: 'count', recurrence_end_count: 30 // 30 days}Use Cases:
Repeat every N weeks (same day of week)
{ is_recurring: true, recurrence_frequency: 'weekly', recurrence_interval: 1, // Every week recurrence_end_type: 'date', recurrence_end_date: '2026-12-31'}Use Cases:
Repeat every N months (same day of month)
{ is_recurring: true, recurrence_frequency: 'monthly', recurrence_interval: 1, // Every month recurrence_end_type: 'never'}Use Cases:
Repeat every N years (same day and month)
{ is_recurring: true, recurrence_frequency: 'yearly', recurrence_interval: 1, // Every year recurrence_end_type: 'count', recurrence_end_count: 5 // 5 years}Use Cases:
Continues indefinitely
{ recurrence_end_type: 'never'}Use Cases:
Ends on specific date
{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31'}Use Cases:
Ends after N occurrences (max 100)
{ recurrence_end_type: 'count', recurrence_end_count: 12 // Maximum 100}Use Cases:
const dailyStandup: EventData = { name: 'Daily Standup', event_date: '2025-12-10', start_time: '09:00', duration: 15, is_recurring: true, recurrence_frequency: 'daily', recurrence_interval: 1, recurrence_end_type: 'never', // Ongoing}const sprintPlanning: EventData = { name: 'Sprint Planning', event_date: '2025-12-10', start_time: '10:00', duration: 120, is_recurring: true, recurrence_frequency: 'weekly', recurrence_interval: 2, // Every 2 weeks recurrence_end_type: 'date', recurrence_end_date: '2026-06-30',}const boardMeeting: EventData = { name: 'Quarterly Board Meeting', event_date: '2026-01-15', start_time: '14:00', duration: 180, is_recurring: true, recurrence_frequency: 'monthly', recurrence_interval: 3, // Every 3 months recurrence_end_type: 'count', recurrence_end_count: 8, // 2 years worth (8 quarters)}const conference: EventData = { name: 'Annual Tech Conference', event_date: '2026-03-15', start_time: '09:00', duration: 480, // 8 hours is_recurring: true, recurrence_frequency: 'yearly', recurrence_interval: 1, recurrence_end_type: 'count', recurrence_end_count: 10, // Next 10 years}Parent Event:
is_recurring = trueparent_event_id = NULLGenerated Instances:
is_recurring = falseparent_event_id = <parent-uuid>// Get only parent events (no instances)const parentEvents = await db.getEvents({ parent_only: true,})// Get all events including instancesconst allEvents = await db.getEvents({ include_instances: true,})// Get all instances of a parent eventconst instances = await db.getEvents({ // Custom query - filter by parent_event_id})// Delete parent eventawait db.deleteEvent(parentEventId)
// All instances automatically deleted via CASCADE// No orphaned instances remain in databaseBenefit: Clean up entire series with single delete operation
Instance generation happens at the application layer using the parent event’s recurrence configuration.
import { getDatabase } from '#libs/database'
async function generateRecurringInstances(parentEventId: string) { const db = getDatabase()
// 1. Fetch parent event const parent = await db.getEventById(parentEventId) if (!parent || !parent.is_recurring) { throw new Error('Not a recurring event') }
// 2. Calculate max instances const maxInstances = parent.recurrence_end_type === 'count' ? parent.recurrence_end_count! : 100 // Default maximum
// 3. Generate instance dates const instances: Date[] = [] const startDate = new Date(parent.event_date)
for (let i = 1; i <= maxInstances; i++) { const instanceDate = new Date(startDate)
// Calculate next occurrence based on frequency switch (parent.recurrence_frequency) { case 'daily': instanceDate.setDate(startDate.getDate() + (i * parent.recurrence_interval)) break case 'weekly': instanceDate.setDate(startDate.getDate() + (i * 7 * parent.recurrence_interval)) break case 'monthly': instanceDate.setMonth(startDate.getMonth() + (i * parent.recurrence_interval)) break case 'yearly': instanceDate.setFullYear(startDate.getFullYear() + (i * parent.recurrence_interval)) break }
// Check end date if (parent.recurrence_end_type === 'date') { const endDate = new Date(parent.recurrence_end_date!) if (instanceDate > endDate) break }
instances.push(instanceDate) }
// 4. Create instances for (const date of instances) { await db.insertEvent({ name: parent.name, event_date: date.toISOString().split('T')[0], start_time: parent.start_time, duration: parent.duration, location: parent.location, description: parent.description, created_by: parent.created_by, parent_event_id: parentEventId, is_recurring: false, }) }
return instances.length}// 1. Create parent eventconst parentId = await db.insertEvent(recurringEventData)
// 2. Generate instancesconst instanceCount = await generateRecurringInstances(parentId)
console.log(`Generated ${instanceCount} instances`)// Update parent event's locationawait db.updateEvent(parentEventId, { location: 'New Conference Room B',})
// Instances NOT automatically updated// Regenerate instances if needed:await db.deleteEventsByParentId(parentEventId)await generateRecurringInstances(parentEventId)// Update one specific occurrenceawait db.updateEvent(instanceId, { start_time: '15:00', // Move this one instance to 3pm})
// Other instances unaffectedThe database enforces these rules:
// ❌ Invalid - missing frequency{ is_recurring: true, // Missing: recurrence_frequency}
// ✅ Valid{ is_recurring: true, recurrence_frequency: 'weekly',}// ❌ Invalid{ recurrence_interval: 0 // Must be > 0}
// ✅ Valid{ recurrence_interval: 2 // Every 2 periods}// ❌ Invalid{ recurrence_end_type: 'date', // Missing: recurrence_end_date}
// ✅ Valid{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31',}// ❌ Invalid{ recurrence_end_type: 'count', recurrence_end_count: 150 // Max is 100}
// ✅ Valid{ recurrence_end_type: 'count', recurrence_end_count: 50,}// ❌ Invalid{ event_date: '2026-01-01', recurrence_end_date: '2025-12-31', // Before event_date}
// ✅ Valid{ event_date: '2026-01-01', recurrence_end_date: '2026-12-31',}// ✅ GOOD - Reasonable limit{ recurrence_end_count: 52 // 1 year of weekly events}
// ❌ BAD - Too many instances{ recurrence_end_count: 100 // Maximum allowed, but may be excessive}// ✅ GOOD - Defined end{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31',}
// ⚠️ CAUTION - No end{ recurrence_end_type: 'never' // Could generate many instances}function validateRecurrence(data: EventData): boolean { if (data.is_recurring) { if (!data.recurrence_frequency) return false if (data.recurrence_end_type === 'date' && !data.recurrence_end_date) return false if (data.recurrence_end_type === 'count' && !data.recurrence_end_count) return false } return true}
if (validateRecurrence(eventData)) { await db.insertEvent(eventData)}try { const instanceCount = await generateRecurringInstances(parentId) console.log(`Successfully generated ${instanceCount} instances`)} catch (error) { console.error('Instance generation failed:', error) // Rollback or cleanup await db.deleteEvent(parentId)}