Event Series & Recurrence
Relevant source files
The following files were used as context for generating this wiki page:
The Event Series & Recurrence system manages calendar events that repeat over time based on specific rules. A Series defines a recurring event once, while an Occurrence represents a single dated instance of that series. This system allows users to create complex schedules, modify individual instances through One-off edits, or truncate a series via a Split.
Sources: GLOSSARY.md:21-25
Data Structures and Models
The system distinguishes between the master definition of a series and its individual instances.
Core Structures
series: Stores the recurrence rules (RRULE), excluded dates (EXDATE), and manually added dates (RDATE).calendarEvent: Represents the rendered state of an event, including its timing, title, and member assignments.oneOff: Represents a modification or cancellation of a specific occurrence within a series.
Sources: internal/app/recurrence.go:14-25, internal/app/calendar.go:94-104
Entity Relationship Diagram
The following diagram shows how master events relate to their modified occurrences.
The events table in the database uses recurring_master_id to link one-off edits back to the parent series. If this ID is null, the row is a standalone event or a series master.
Sources: internal/app/events.go:43-52, internal/app/ics.go:34-40
Recurrence Expansion Logic
GoCasa uses the rrule logic to calculate which events appear in a given time window. The expand function merges the rule-generated instances with manually defined overrides.
Expansion Process
- Generate base instances: The system iterates from the
DTSTART(start date) of the series using therrule-golibrary. - Filter by window: It keeps only instances touching the requested
[from, to)time range. - Apply Exclusions: It removes any dates listed in the
EXDATEfield. - Merge One-off edits: It replaces rule-generated instances with modified versions from the database or hides them if they are marked as "cancelled".
- Add RDATEs: It inserts extra instances manually defined in the
RDATEfield.
Sources: internal/app/recurrence.go:39-106%60%20time%20range.%0A3.%20%20Apply%20Exclusions%3A%20It%20removes%20any%20dates%20listed%20in%20the%20%60EXDATE%60%20field.%0A4.%20%20Merge%20One-off%20edits%3A%20It%20replaces%20rule-generated%20instances%20with%20modified%20versions%20from%20the%20database%20or%20hides%20them%20if%20they%20are%20marked%20as%20%22cancelled%22.%0A5.%20%20Add%20RDATEs%3A%20It%20inserts%20extra%20instances%20manually%20defined%20in%20the%20%60RDATE%60%20field.%0A%0ASources%3A%20%5Binternal/app/recurrence.go%3A39-106)
Recurrence Constraints
| Constraint | Value | Description |
|---|---|---|
| Max Occurrences | 1000 | The maximum number of instances expanded for a single series in one window. |
| Horizon Years | 100 | The system stops expanding a series 100 years past its start date. |
| Max Frequency | DAILY | A Series repeats at most daily; sub-daily frequencies (hourly) are rejected. |
Sources: internal/app/recurrence.go:10-12, internal/app/calendar.go:273-275
Editing Scopes
When a user modifies a recurring event, they must choose an Edit Scope. This determines how the changes propagate through the series.
| Scope | Resulting Action |
|---|---|
| This event | Creates or updates a oneOff row for the specific instance. It does not affect other occurrences. |
| This and following | Performs a Split. The existing series is truncated with an UNTIL rule, and a new independent series starts from the chosen date. |
| All events | Updates the master series row. This scope only allows changing metadata (title, description, location, members) and excludes timing or recurrence changes. |
Sources: GLOSSARY.md:37-41, internal/app/events.go:189-224, internal/web/calendar_templ.go:420-438
Split Operation Flow
The following diagram illustrates the "This and following" logic which results in a series split.
Time Zone and DST Handling
GoCasa stores timed events using UTC timestamps and IANA time zone identifiers.
Floating Times
To preserve the "wall clock" time (e.g., 09:00 AM) across Daylight Saving Time (DST) shifts, the expansion logic uses "floating" UTC times. It converts the UTC timestamp to the local wall time, performs the recurrence calculation in UTC to avoid DST skips, and then converts back to the correct offset for the target date.
Sources: internal/app/recurrence.go:27-31, internal/app/recurrence.go:56-58
Validation
- Gap times: The system refuses times that do not exist due to a "spring forward" clock change.
- Ambiguous times: For "fall back" overlaps, the system picks the first occurrence of the ambiguous time.
Sources: internal/app/calendar.go:318-320, internal/app/calendar.go:51-62
iCalendar (ICS) Export
The system exports series and occurrences in standard iCalendar format (RFC 5545).
- Series: Exported as a single
VEVENTwith anRRULEproperty. - One-off edits: Exported as separate
VEVENTcomponents sharing the sameUIDas the master, but identified by aRECURRENCE-IDproperty. - Cancellations: Deleted occurrences are added to the master event's
EXDATElist.
Sources: internal/app/ics.go:21-25, internal/app/ics.go:81-85
Conclusion
The Event Series & Recurrence system provides a robust implementation of the iCalendar specification, supporting complex recurrence rules while allowing granular control over individual instances. By utilizing a master-override database pattern and explicit editing scopes, it maintains data integrity during series modifications and ensures consistent behavior across time zone transitions.