Skip to content

Recurring Events

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 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.

import { getDatabase } from '#libs/database'
import type { EventData } from '#libs/database'
const db = getDatabase()
// Weekly meeting for 12 weeks
const 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:

  • Daily standup meetings
  • Medication reminders
  • Exercise routines

Continues indefinitely

{
recurrence_end_type: 'never'
}

Use Cases:

  • Ongoing meetings
  • Continuous schedules
  • No planned end date
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:

  • Defines recurrence pattern
  • is_recurring = true
  • parent_event_id = NULL
  • Serves as template for instances

Generated Instances:

  • Specific occurrences
  • is_recurring = false
  • parent_event_id = <parent-uuid>
  • Inherit parent’s properties but have unique dates
// Get only parent events (no instances)
const parentEvents = await db.getEvents({
parent_only: true,
})
// Delete parent event
await db.deleteEvent(parentEventId)
// All instances automatically deleted via CASCADE
// No orphaned instances remain in database

Benefit: 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 event
const parentId = await db.insertEvent(recurringEventData)
// 2. Generate instances
const instanceCount = await generateRecurringInstances(parentId)
console.log(`Generated ${instanceCount} instances`)
// Update parent event's location
await 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 occurrence
await db.updateEvent(instanceId, {
start_time: '15:00', // Move this one instance to 3pm
})
// Other instances unaffected

The database enforces these rules:

  1. Recurring must have frequency:
// ❌ Invalid - missing frequency
{
is_recurring: true,
// Missing: recurrence_frequency
}
// ✅ Valid
{
is_recurring: true,
recurrence_frequency: 'weekly',
}
  1. Interval must be positive:
// ❌ Invalid
{
recurrence_interval: 0 // Must be > 0
}
// ✅ Valid
{
recurrence_interval: 2 // Every 2 periods
}
  1. 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',
}
  1. 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,
}
  1. 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',
}
// ✅ 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)
}