This Google Summer of Code 2025 project significantly enhanced the Talawa platform's event management system by implementing comprehensive support for recurring events, action items, and volunteer coordination. The work focused on both backend (talawa-api) and frontend (talawa-admin) components, introducing advanced features for event scheduling, task management, and community engagement.
- Create recurring events, generation window, cron job and query
- Create and query recurring events (frontend)
- Delete recurring events
- Delete recurring events (frontend)
- Update recurring events
- Update recurring events (frontend)
- Implemented CRUD operations for volunteers, volunteer groups, and volunteer membership (talawa-api)
- Volunteers, volunteer groups, and volunteer membership feature in admin screen and user portal (talawa-admin)
- There are two kinds of events:
- Standalone events: single occurrence stored directly in
events. - Recurring events: defined by a template in
events+ a recurrence rule inrecurrence_rules, with materialized child instances inrecurring_event_instances.
- Standalone events: single occurrence stored directly in
- Users never edit the template rows directly. They work with instances or with high-level actions (entire series, this and following, this instance only).
- A background worker pre-creates instances within a 12‑month "hot window" and keeps cleaning old ones. Cron runs hourly (generation) and daily (cleanup).
- Updates support three scopes: this instance only, entire series (name/description), and this and following (split the series).
- Deletes support three scopes: this instance only, this and following, and entire series.
Tables in talawa-api (Drizzle ORM):
-
events- Stores both standalone events and recurring templates.
is_recurring_template(akaisRecurringEventTemplate) tells if a row is a recurring template.- Standalone events:
is_recurring_template = falseand their ownstart_at/end_atare final. - Recurring templates:
is_recurring_template = trueand act as the parent/defaults for instances.
-
recurrence_rules- One per recurring template.
- Holds the RRULE-like data (
frequency,interval,byDay,byMonth,byMonthDay,recurrenceStartDate, optionalrecurrenceEndDateorcount). base_recurring_event_idlinks back to the template inevents.original_series_idties together all splits of the same logical series across time.
-
recurring_event_instances- Materialized occurrences for recurring series inside the hot window.
- Key fields:
base_recurring_event_id→ templaterecurrence_rule_id→ source ruleoriginal_series_id→ logical series identity across splitsoriginal_instance_start_time→ when the RRULE said it should startactual_start_time/actual_end_time→ after applying exceptions and durationsequence_number,total_count(if finite),is_cancelled
-
event_exceptions- Stores per-instance overrides when a user edits a single occurrence (e.g., rename just one date, shift time, etc.).
- Exception JSON holds only the fields that differ from the template.
-
event_generation_windows- Per-organization config for the hot window and retention:
hot_window_months_aheaddefault 12 (1 year forward)history_retention_monthsdefault 3 (cleanup window)- Tracks window boundaries and processing metadata.
- Per-organization config for the hot window and retention:
-
Standalone event
- Single row in
events, shown directly in calendars and lists. - Users edit/delete the event itself.
- Single row in
-
Recurring template (parent)
- Row in
eventswithis_recurring_template = true. - Paired with a
recurrence_rulesrow. - Not directly shown or edited by users as an event occurrence.
- Row in
-
Recurring instance (child)
- Row in
recurring_event_instancescreated by the background worker. - Inherits default fields (name, description, location, visibility, registerable, etc.) from the template.
- May have a matching
event_exceptionsrow to override specific fields for that one occurrence. - What users usually see and interact with on the calendar.
- Row in
Talawa pre-creates occurrences for recurring events in a forward window to make queries fast and simple.
- Window: The default hot window is 12 months ahead; retention keeps ~3 months of history. Config lives in
event_generation_windows. - Cron: A background worker runs on a schedule (node-cron). Defaults:
- Generation: hourly (
EVENT_GENERATION_CRON_SCHEDULE, default0 * * * *). - Cleanup: daily at 02:00 UTC (
CLEANUP_CRON_SCHEDULE, default0 2 * * *).
- Generation: hourly (
- Pipeline (high level):
- Discover work: find orgs whose window needs extension and their recurring templates.
- Normalize recurrence rules (convert COUNT to a derived end date for consistent window math).
- Calculate all occurrences between window start and window end.
- Insert only missing instances (dedupe by
original_instance_start_time). - Update housekeeping (window end, last processed counts, etc.).
- Key behavior:
- If a rule is never-ending (no end date and no count), we only generate up to window end.
- If a rule has COUNT, the engine estimates/derives completion and stops accordingly.
- Instances store both the original schedule and the final start/end — so exceptions and re-calculations remain consistent.
Three user-facing options map to distinct backend behaviors.
- Update this event only
- Mutation:
updateSingleRecurringEventInstance - What happens:
- We write an
event_exceptionsrow for that instance with only changed fields (e.g.,name,description,location,allDay,isPublic,isRegisterable,startAt,endAt). - If times are changed, we also update
actual_start_time/actual_end_timeon the instance. - Other instances are unaffected.
- We write an
- Update entire series (name/description)
- Mutation:
updateEntireRecurringEventSeries - What happens:
- We resolve the logical series via
original_series_idwhich is template. - We update only safe, series-wide fields on all base templates across the whole logical series (even if the series was split earlier):
- Supported:
name,description - Not changed here: timing/recurrence/location/visibility/registration flags
- Supported:
- We bump
last_updated_aton instances so caches and clients know something changed. - Because instances inherit name/description, the change is reflected everywhere (past/present/future), unless an instance has its own exception for those fields.
- We resolve the logical series via
- Update this and following (split the series)
- Mutation:
updateThisAndFollowingEvents - What happens:
- We “cut” the current template’s recurrence by setting its
recurrence_end_dateto just before the targeted instance. - We delete materialized instances from this point forward for the current template.
- We create a brand-new template event, apply provided changes (name, desc, location, timing, flags), and create a new
recurrence_rulesrow for it. - We set the new rule’s
original_series_id(used as the logical series identifier going forward). - The background worker immediately generates future instances for the new template within the hot window.
- Net effect in the UI: everything before the split stays as-is under the old template; the targeted instance becomes the first occurrence of a new series with your updates; everything after follows the new pattern.
- We “cut” the current template’s recurrence by setting its
Notes on original_series_id
- This field links templates and instances that belong to the same logical series across splits.
- Actions like “update entire series” and “delete this and following” consult
original_series_idto operate across all templates created from earlier splits.
Three user-facing options:
-
Delete this event only
- Mutation:
deleteSingleEventInstance - What happens:
- We delete action items linked to this instance.
- We set
is_cancelled = trueon the instance (kept for history/reporting), and updatelast_updated_at. - Other instances/series are unaffected.
- Mutation:
-
Delete this and following
- Mutation:
deleteThisAndFollowingEvents - What happens:
- We set the current rule's
recurrence_end_dateto just before this occurrence (effectively trimming the series). - We find all instances with the same
original_series_idwhoseactual_start_time >=this instance and delete them. - We also delete their exceptions and action items.
- Past instances remain.
- We set the current rule's
- Mutation:
-
Delete entire series
- Mutation:
deleteEntireRecurringEventSeries - Input is the template (base event) ID.
- What happens:
- Using the template's rule, we get its
original_series_id. - We delete, in order: action items (for all templates and instances), instance exceptions, all instances in the series, all recurrence rules in the series, and finally all template rows in
eventsthat belong to this logical series. - Attachments for the requested template are cleaned up from storage.
- Using the template's rule, we get its
- Mutation:
- The GraphQL
Eventtype represents both standalone events and recurring instances with a unified shape. - For recurring instances, "resolved" fields come from: template defaults + per-instance exceptions + instance timing.
- Useful instance metadata:
sequenceNumber(e.g., "5 of 12"),totalCountwhen finite.hasExceptionsandappliedExceptionDataflags to indicate customizations.
- Generation cron (hourly by default) grows the forward window and materializes missing instances.
- Cleanup cron (daily by default) removes old instances beyond
history_retention_monthsfor each org and advancesretention_start_date. - Defaults live in
event_generation_windowsand can be tuned per organization.
- Never-ending series: only materialized up to the current hot window; cron extends as time moves on.
- Count-based rules: internally normalized to derive an end date for consistent generation; the original count semantics are preserved when calculating occurrences.
- Instance de-duplication: before inserting, generation checks for existing
original_instance_start_timewithin the window to avoid duplicates.
If you need deeper implementation details, see these backend modules:
- Tables:
src/drizzle/tables/events.ts,recurrenceRules.ts,recurringEventInstances.ts,recurringEventExceptions.ts,eventGenerationWindows.ts - Generation service:
src/services/eventGeneration/* - Background workers and cron:
src/workers/backgroundWorkerService.ts,src/workers/eventGeneration/*,src/workers/eventCleanupWorker.ts - GraphQL mutations:
src/graphql/types/Mutation/*RecurringEvent*and related inputs.
This guide explains how action items relate to events in Talawa, with a focus on recurring series and per-instance behavior. It complements events.md.
- Two kinds of action items:
- Series (template) action items: attached to the recurring event template; inherit to every instance unless overridden.
- Instance-only action items: attached directly to one recurring instance (or a standalone event).
- Per-instance changes to a series item are stored as action item exceptions (override, complete, or delete just for that date).
- Reading an instance's action items = series items ± exceptions + instance-only items.
- Deletions cascade appropriately:
- Delete instance → removes its instance-only items and exceptions.
- Delete this and following → removes items/exceptions for affected future instances.
- Delete entire series → removes template items, all instance-only items, and all exceptions across the series.
Tables in talawa-api (Drizzle ORM):
-
actionitems- Series-level:
eventId= template event id;isTemplate=true(advisory flag). - Instance-only:
recurringEventInstanceId= instance id (no inheritance). - Other fields:
assignedAt,preCompletionNotes,postCompletionNotes,isCompleted,volunteerId,volunteerGroupId,categoryId,organizationId,creatorId,updaterId.
- Series-level:
-
actionitem_exceptions- One row per (actionId, recurring instance) when a series item is overridden for a specific occurrence.
- Fields:
completed,deleted,assignedAt,preCompletionNotes,postCompletionNotes,volunteerId,volunteerGroupId,categoryId,assigneeId. - Unique(actionId, eventId) enforces one exception per action item per instance.
-
recurring_event_instances- Materialized occurrences for recurring events; used to anchor instance-only items and exceptions.
- Action item: A task/todo associated with an event (title, description/notes, status, assignees, due date, etc.).
- Standalone event action item: Attached directly to a single, non-recurring event.
- Template (series) action item: Attached to a recurring event template and inherited by each instance.
- Instance action item: Attached to one specific occurrence ("this instance only"). Also called a standalone action item for that instance.
- Action item exception: A per-instance override for a template action item (e.g., changed title/status for a particular date, or suppressed/deleted just for that one instance).
-
Standalone events
- Action items are linked directly to the event. No recurrence logic is involved.
-
Recurring events
- Two scopes are supported when creating action items:
- Series (template): Create a template action item that applies to all instances by default.
- This instance only: Create an instance action item that exists only for the selected occurrence.
- Reading an instance's action items resolves as:
- Start with all template action items for the series.
- Apply any action item exceptions for this specific instance (overrides or suppressions).
- Add any standalone instance action items.
- Two scopes are supported when creating action items:
- Determine baseEventId = instance.baseRecurringEventId (for recurring) or event.id (for standalone).
- Query
actionitemswhere:eventId = baseEventId(series/template items), ORrecurringEventInstanceId = instance.id(instance-only items).
- Query
actionitem_exceptionswhereactionId IN (series items)ANDeventId = instance.id. - For each series item:
- If exception.deleted = true → exclude for this instance.
- Else override provided fields (completed, notes, assignedAt, volunteer/group/category, assignee).
- Return final list (series-with-overrides + instance-only), then paginate.
- Series (template) action item
- Attaches to the template; appears on every instance unless overridden/suppressed by an exception.
- Instance-only action item
- Attaches to the selected occurrence; no impact on other dates.
- Update an instance-only action item
- Directly updates that single record; no series impact.
- Update a template action item for this instance only
- Creates or updates an action item exception for that instance which overrides the template item (e.g., changed status to Completed, edited notes, adjusted due date).
- Update a template action item for the entire series
- Edits the template record; affects all instances except those with per-instance exceptions for the changed fields.
- Completing a template action item for this instance only
- Records an exception on that instance, setting its status to Completed; other instances remain unchanged.
- Completing a template action item for the entire series
- Marks the template as Completed; instances reflect completion unless an instance has an exception stating otherwise.
- Delete an instance-only action item
- Removes only that occurrence's action item.
- Delete a template action item for this instance only
- Records an exception that suppresses that template action item for the chosen instance (it is treated as deleted only for this date).
- Delete a template action item for the entire series
- Removes the template item; it disappears from all instances except where a per-instance action item (standalone) exists.
- When a series is split, the new series starts fresh with its own template action items.
- Existing instance-only items and exceptions remain attached to the old instances; no automatic copying of template items to the new series unless explicitly implemented.
- For any given instance, the resolved action item list is computed by:
- Listing all template (series) action items.
- Applying per-instance exceptions (overrides/complete/suppress) to those template items.
- Adding all instance-only action items.
- This is analogous to how event field exceptions work for recurring instances.
Key tables and fields used to associate action items with events and instances:
-
actionitems (
src/drizzle/tables/actionItems.ts)- eventId: UUID → references
events.id(recurring template or standalone event) - recurringEventInstanceId: UUID → references
recurring_event_instances.id(instance-only action item) - isTemplate: boolean → when true, indicates a series-level (template) action item
- organizationId, creatorId, updaterId, assignedAt, preCompletionNotes, postCompletionNotes, isCompleted, categoryId, volunteerId, volunteerGroupId
- eventId: UUID → references
-
actionitem_exceptions (
src/drizzle/tables/actionItemExceptions.ts)- actionId: UUID → references
actionitems.id(the series-level action item being overridden) - eventId: UUID → references
recurring_event_instances.id(the specific instance where the override applies) - Unique(actionId, eventId) → one override per action item per instance
- Fields mirrored for overrides: assignedAt, preCompletionNotes, postCompletionNotes, completed, deleted, volunteerId, volunteerGroupId, categoryId, assigneeId
- actionId: UUID → references
-
recurring_event_instances (
src/drizzle/tables/recurringEventInstances.ts)- Holds materialized instances for recurring events, used to scope exceptions and instance-only action items.
Resolver: src/graphql/types/Event/actionItems.ts
- For a given Event node (standalone or instance):
- Determine baseEventId = parent.baseRecurringEventId || parent.id
- Query actionitems where:
- eventId = baseEventId (series-level items), OR
- recurringEventInstanceId = parent.id (instance-only items)
- Fetch actionitem_exceptions for the current instance: where actionId IN (above) AND eventId = parent.id
- Apply per-item logic:
- If exception.deleted = true → exclude this item for this instance
- Otherwise override fields (completed, assignedAt, notes, volunteer/volunteerGroup/category, etc.)
- Return paginated list
- completeActionItemForInstance → sets exception.completed = true and postCompletionNotes
- markActionItemAsPendingForInstance → sets exception.completed = false
- updateActionItemForInstance → upserts instance-specific overrides (assignedAt, preCompletionNotes, volunteer, group, category)
- deleteActionItemForInstance → sets exception.deleted = true (suppresses series item just for this date)
- deleteActionItem → deletes the template action item, after first deleting all its exceptions
-
Tables
- actionItems:
src/drizzle/tables/actionItems.ts - actionItemExceptions:
src/drizzle/tables/actionItemExceptions.ts - recurringEventInstances:
src/drizzle/tables/recurringEventInstances.ts
- actionItems:
-
Resolvers
- Event.actionItems:
src/graphql/types/Event/actionItems.ts
- Event.actionItems:
-
Mutations (instance scope)
- completeActionItemForInstance:
src/graphql/types/Mutation/completeActionItemForInstance.ts - markActionItemAsPendingForInstance:
src/graphql/types/Mutation/markActionItemAsPendingForInstance.ts - updateActionItemForInstance:
src/graphql/types/Mutation/updateActionItemForInstance.ts - deleteActionItemForInstance:
src/graphql/types/Mutation/deleteActionItemForInstance.ts
- completeActionItemForInstance:
-
Mutations (template scope)
- deleteActionItem:
src/graphql/types/Mutation/deleteActionItem.ts
- deleteActionItem:
-
Mutations (event lifecycle)
- deleteSingleEventInstance:
src/graphql/types/Mutation/deleteSingleEventInstance.ts - deleteThisAndFollowingEvents:
src/graphql/types/Mutation/deleteThisAndFollowingEvents.ts - deleteEntireRecurringEventSeries:
src/graphql/types/Mutation/deleteEntireRecurringEventSeries.ts
- deleteSingleEventInstance:
This guide explains how individual volunteers, volunteer groups, and memberships work for events in Talawa, including recurring series behavior.
- Two attachment scopes for recurring events:
- Entire series (template): volunteer/group applies to every instance unless explicitly excluded for specific dates.
- This instance only: volunteer/group exists only for one occurrence.
- Per-instance removal uses "exceptions" rows rather than deleting the series record.
Tables in talawa-api (Drizzle ORM):
-
event_volunteers- One row per user volunteering for an event context.
- Template volunteer (series-wide):
eventId= base template event id,isTemplate = true,recurringEventInstanceId = null. - Instance-only volunteer:
eventId= base template event id,isTemplate = false,recurringEventInstanceId = instance id. - Other fields:
hasAccepted,isPublic,hoursVolunteered,creatorId,updaterId. - Unique(userId, eventId, recurringEventInstanceId) prevents duplicates across series vs one instance.
-
event_volunteer_groups- Volunteer group definition for an event context.
- Template group (series-wide):
eventId= base template event id,isTemplate = true,recurringEventInstanceId = null. - Instance-only group:
eventId= base template event id,isTemplate = false,recurringEventInstanceId = instance id. - Other fields:
leaderId,name,description,volunteersRequired,creatorId,updaterId. - Unique(eventId, name, recurringEventInstanceId) prevents name collisions per instance.
-
event_volunteer_memberships- Connects an EventVolunteer to an optional EventVolunteerGroup for an event.
- Fields:
volunteerId,groupId(nullable for individual volunteers),eventId,status(invited/requested/accepted/rejected), audit fields. - Unique(volunteerId, groupId, eventId).
- Note: in create requests,
eventIdis stored as provided (instance or template) to support admin requests UI; the volunteer row itself is linked to the base event id.
-
Exceptions tables (per-instance exclusions)
event_volunteer_exceptions: unique(volunteerId, recurringEventInstanceId). Marks a template volunteer as "excluded" on a specific instance.event_volunteer_group_exceptions: unique(volunteerGroupId, recurringEventInstanceId). Marks a template group as "excluded" on a specific instance.
-
recurring_event_instances- Materialized occurrences for recurring events; anchor for instance-only volunteers/groups and per-instance exceptions.
- Event Volunteer: a user attached to an event series or a single occurrence.
- Volunteer Group: a named group with a leader, optionally with a target number of volunteers required.
- Volunteer Membership: the relationship (with status) between a volunteer and a group and/or event; supports invitations and requests.
- Template (series-wide): applies automatically to every past/present/future instance unless excluded by an exception for a specific date.
- Instance-only: exists only for a single occurrence.
- Exception: per-instance exclusion record that hides a template volunteer/group from one occurrence.
- Determine baseEventId = instance.baseRecurringEventId (for recurring) or event.id (for standalone).
- Volunteers for a recurring instance:
- Query
event_volunteerswhereeventId = baseEventId(template volunteers) ORrecurringEventInstanceId = instance.id(instance-only volunteers). - Query
event_volunteer_exceptionswhererecurringEventInstanceId = instance.idand exclude those volunteer IDs from the template set.
- Query
- Volunteer groups for a recurring instance:
- Query
event_volunteer_groupswhereeventId = baseEventId(template groups) ORrecurringEventInstanceId = instance.id(instance-only groups). - Query
event_volunteer_group_exceptionswhererecurringEventInstanceId = instance.idand exclude those group IDs from the template set.
- Query
- For standalone events, simply query by
eventId = event.idand ignore exceptions/instances.
- Create
createEventVolunteersupports ascope:- ENTIRE_SERIES → create or reuse a template volunteer (
isTemplate=true,recurringEventInstanceId=null), converts/removes prior instance-only duplicates for the same user/event. - THIS_INSTANCE_ONLY → create an instance-only volunteer for
recurringEventInstanceId.
- ENTIRE_SERIES → create or reuse a template volunteer (
- Update
updateEventVolunteerupdates fields such ashasAccepted,isPublicfor the specific volunteer row (template or instance-only).
- Delete
- Entire row:
deleteEventVolunteerremoves the volunteer and cascades memberships. - For one instance of a template volunteer:
deleteEventVolunteerForInstanceupserts an exception row to exclude only that date; the template volunteer remains for all other dates.
- Entire row:
- Create
createEventVolunteerGroupsupports ascope:- ENTIRE_SERIES → create or reuse a template group (
isTemplate=true), converts/removes prior instance-only duplicates with same name. - THIS_INSTANCE_ONLY → create an instance-only group for
recurringEventInstanceId.
- ENTIRE_SERIES → create or reuse a template group (
- Optional
volunteerUserIdsallows inviting/creating volunteers and adding memberships to the new group, matching old API behavior.
- Update
updateEventVolunteerGroupupdates name/description/volunteersRequired; checks for name conflicts within the same event context.
- Delete
- Entire row:
deleteEventVolunteerGroupremoves the group and cascades its memberships. - For one instance of a template group:
deleteEventVolunteerGroupForInstanceupserts an exception row to exclude only that date; the template group remains for other dates.
- Entire row:
- Create
createVolunteerMembershipcreates a membership withstatus(invited/requested/accepted/rejected).- If no
EventVolunteerexists yet for the user + base event, it creates one (template vs instance-only determined byscopeandrecurringEventInstanceId). - Stores
eventIdas provided (instance or template) to support admin review screens.
- Update
updateVolunteerMembershipupdates thestatus. If accepted, the linkedEventVolunteer.hasAcceptedis synced appropriately.
-
Tables
- eventVolunteers:
src/drizzle/tables/eventVolunteers.ts - eventVolunteerGroups:
src/drizzle/tables/eventVolunteerGroups.ts - eventVolunteerMemberships:
src/drizzle/tables/eventVolunteerMemberships.ts - eventVolunteerExceptions:
src/drizzle/tables/eventVolunteerExceptions.ts - eventVolunteerGroupExceptions:
src/drizzle/tables/eventVolunteerGroupExceptions.ts - recurringEventInstances:
src/drizzle/tables/recurringEventInstances.ts
- eventVolunteers:
-
Resolvers (read path)
- Event.volunteers:
src/graphql/types/Event/volunteers.ts - Event.volunteerGroups:
src/graphql/types/Event/volunteerGroups.ts
- Event.volunteers:
-
Mutations (volunteers)
- createEventVolunteer:
src/graphql/types/Mutation/createEventVolunteer.ts - updateEventVolunteer:
src/graphql/types/Mutation/updateEventVolunteer.ts - deleteEventVolunteer:
src/graphql/types/Mutation/deleteEventVolunteer.ts - deleteEventVolunteerForInstance:
src/graphql/types/Mutation/deleteEventVolunteerForInstance.ts
- createEventVolunteer:
-
Mutations (volunteer groups)
- createEventVolunteerGroup:
src/graphql/types/Mutation/createEventVolunteerGroup.ts - updateEventVolunteerGroup:
src/graphql/types/Mutation/updateEventVolunteerGroup.ts - deleteEventVolunteerGroup:
src/graphql/types/Mutation/deleteEventVolunteerGroup.ts - deleteEventVolunteerGroupForInstance:
src/graphql/types/Mutation/deleteEventVolunteerGroupForInstance.ts
- createEventVolunteerGroup:
-
Mutations (memberships)
- createVolunteerMembership:
src/graphql/types/Mutation/createVolunteerMembership.ts - updateVolunteerMembership:
src/graphql/types/Mutation/updateVolunteerMembership.ts
- createVolunteerMembership: