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.
Overview
Section titled “Overview”Recurring events provide:
- Flexible Frequencies: Daily, weekly, monthly, yearly patterns
- Custom Intervals: Repeat every N days/weeks/months/years
- End Conditions: Never-ending, date-based, or count-based
- Instance Generation: Automatic instance creation for recurring series
- Cascade Deletion: Remove all instances when parent is deleted
📆 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.
Quick Start
Section titled “Quick Start”Creating Recurring Events
Section titled “Creating Recurring Events”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)Recurrence Patterns
Section titled “Recurrence Patterns”Supported Frequencies
Section titled “Supported Frequencies”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:
- Daily standup meetings
- Medication reminders
- Exercise routines
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:
- Weekly team meetings
- Class schedules
- Recurring appointments
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:
- Monthly board meetings
- Billing cycles
- Newsletter schedules
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:
- Annual events
- Birthdays/anniversaries
- Yearly reviews
End Conditions
Section titled “End Conditions”Continues indefinitely
{ recurrence_end_type: 'never'}Use Cases:
- Ongoing meetings
- Continuous schedules
- No planned end date
Ends on specific date
{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31'}Use Cases:
- Semester schedules
- Contract-based events
- Time-bound projects
Ends after N occurrences (max 100)
{ recurrence_end_type: 'count', recurrence_end_count: 12 // Maximum 100}Use Cases:
- Fixed-length programs
- Course schedules
- Limited series
Common Patterns
Section titled “Common Patterns”Daily Standup
Section titled “Daily Standup”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}Bi-Weekly Sprint Planning
Section titled “Bi-Weekly Sprint Planning”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',}Quarterly Board Meeting
Section titled “Quarterly Board Meeting”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)}Annual Conference
Section titled “Annual Conference”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-Child Relationships
Section titled “Parent-Child Relationships”Understanding the Model
Section titled “Understanding the Model”Parent Event:
- Defines recurrence pattern
is_recurring = trueparent_event_id = NULL- Serves as template for instances
Generated Instances:
- Specific occurrences
is_recurring = falseparent_event_id = <parent-uuid>- Inherit parent’s properties but have unique dates
Querying
Section titled “Querying”// 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})Cascade Deletion
Section titled “Cascade Deletion”// 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
Section titled “Instance Generation”Instance generation happens at the application layer using the parent event’s recurrence configuration.
Example Implementation
Section titled “Example Implementation”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`)Updating Recurring Events
Section titled “Updating Recurring Events”Update Parent Event
Section titled “Update Parent Event”// 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 Single Instance
Section titled “Update Single Instance”// Update one specific occurrenceawait db.updateEvent(instanceId, { start_time: '15:00', // Move this one instance to 3pm})
// Other instances unaffectedConstraints & Validation
Section titled “Constraints & Validation”Database-Level Validation
Section titled “Database-Level Validation”The database enforces these rules:
- Recurring must have frequency:
// ❌ Invalid - missing frequency{ is_recurring: true, // Missing: recurrence_frequency}
// ✅ Valid{ is_recurring: true, recurrence_frequency: 'weekly',}- Interval must be positive:
// ❌ Invalid{ recurrence_interval: 0 // Must be > 0}
// ✅ Valid{ recurrence_interval: 2 // Every 2 periods}- End date required when end_type is ‘date’:
// ❌ Invalid{ recurrence_end_type: 'date', // Missing: recurrence_end_date}
// ✅ Valid{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31',}- End count between 1-100:
// ❌ Invalid{ recurrence_end_type: 'count', recurrence_end_count: 150 // Max is 100}
// ✅ Valid{ recurrence_end_type: 'count', recurrence_end_count: 50,}- End date must be after event date:
// ❌ 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',}Best Practices
Section titled “Best Practices”1. Limit Instance Count
Section titled “1. Limit Instance Count”// ✅ 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}2. Use End Conditions
Section titled “2. Use End Conditions”// ✅ GOOD - Defined end{ recurrence_end_type: 'date', recurrence_end_date: '2026-12-31',}
// ⚠️ CAUTION - No end{ recurrence_end_type: 'never' // Could generate many instances}3. Validate Before Creation
Section titled “3. Validate Before Creation”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)}4. Handle Instance Generation Errors
Section titled “4. Handle Instance Generation Errors”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)}Next Steps
Section titled “Next Steps”- Events System - Learn about basic event management
- Database Architecture - Understand the abstraction layer
- Database Switching - Switch between providers
Technical References
Section titled “Technical References”- Database API Reference - Complete API documentation
- Events Table Feature - Recurring events schema details
- Migration 010 - Recurrence migration details