Auburn MyGov Docs Gitea

Layer

Calendar & Time

The time layer. Calendars are shared publication surfaces — independent of any single module — that all time-based content links into. Programs, Boards, Holidays, and Reservations each contribute entries; the calendar aggregates them into unified views.

Status In Progress
Last updated
Related LayerLocation · Distribution & Publishing
PlatformMicrosoft Platform
ModuleReservations · Programs & Registration

What a Calendar Is

A Calendar is a named, configurable container for time-based entries. It is not the authoritative source for any record — it is a shared aggregation surface. Programs, Boards, Holidays, and Reservations all maintain their own modules; the Calendar is where those records become visible together in a time-based view.

This independence is the defining characteristic. The City Meetings calendar does not belong to the Boards module — it belongs to the Calendar layer. Boards push into it. If tomorrow a new module (Road Closures, Special Events) needs time-based visibility, it pushes into a Calendar too — no changes to the Calendar layer are required.

Calendars support a parent/child hierarchy. A parent calendar aggregates entries from its children without requiring staff to enter anything twice. A child belongs to at most one parent.

Calendar Types

Calendars serve several distinct purposes across the platform. The type is not a formal schema field — it is a description of the calendar's primary source and audience.

Source: Programs module
Programs Calendars

Parks and Library program sessions. Each program category maps to a calendar. A parent calendar (e.g. "Parks Programs") aggregates all child program calendars.

Source: Boards module
City Meetings

Committee and board meeting agendas. The Boards module publishes meeting dates and documents; meetings appear on the City Meetings calendar. Independent of Programs.

Source: Staff-authored
City Holidays

Official holidays and observed dates. Staff-authored native events. Future: changes here propagate as exception hours on affected Facility records.

Source: Reservations + Programs
Facility / Space Calendars

One calendar per Facility or FacilitySpace showing all events and reservations at that location. Feeds the facility detail page and the Exchange resource mailbox for reservable spaces.

Source: External ICS feed
Imported Calendars

External webcal/ICS subscriptions polled on a schedule. Used for city council streams, county feeds, or partner organization events. MyGov caches the entries; the external source is authoritative.

Source: Staff-authored
Native Event Calendars

One-off events authored directly in the Calendar module — visiting acts, community events, road closures. Not tied to any other module.

Future: Holiday → Facility exception hours

When a holiday is added to the City Holidays calendar, affected Facility records should have their hours updated automatically (or with a confirmation prompt) to reflect the closure. This creates a direct link between the Calendar layer and the Location layer — Facility hours become a derived view of operating schedule plus holiday exceptions, rather than a manually maintained field. This is a design target that requires the FacilityContact.hours JSON to support a typed exception model; the current free-form JSON blob would need to be formalised first.

Calendar Entry Data Model

All calendar entries share a common schema regardless of their source. CalendarEvent is an iCal-compatible core augmented with a small, deliberately capped set of optional structured fields for location linkage, source provenance, content categorisation, and web display.

FieldTypeiCal mappedNotes
uidstringUIDGlobally unique; stable across sync. Derived from sourceType-sourceId for pushed entries.
summarystringSUMMARYEvent title.
dtstart / dtenddatetimeDTSTART / DTENDTimezone-aware.
rrulestringRRULEOptional. iCal recurrence rule.
exdatedatetime[]EXDATEOptional. Exception dates excluded from a recurring series.
locationstringLOCATIONOptional free text. Auto-composed from FacilitySpace + Facility address when facilitySpaceId is set.
descriptionstringDESCRIPTIONOptional plain text. Included in iCal export and Outlook event body.
urlstringURLOptional link to the source record or detail page.
categoriesstring[]CATEGORIESOptional. iCal CATEGORIES field — populated from categoryId label at export time. Clients that honour it (e.g. Apple Calendar) display the category; Outlook ignores it gracefully.
facilitySpaceIdint?Optional FK to organization.FacilitySpace. Enables structured location display and facility-calendar views. Not in iCal export.
categoryIdint?Optional FK to data.CalendarCategory. Used for filtering, display colour/icon, and populating the iCal CATEGORIES field. Not propagated to Outlook as a shared colour — see Category & Tagging decision below.
sourceTypestring?Optional. Identifies the pushing module: programSession | reservation | boardMeeting | native | icsFeed. Used for deduplication and reverse lookup.
sourceIdint?Optional FK back to the source record. Paired with sourceType.
imageUrlstring?Optional. Web display only — not in iCal export. Inherited from the source record at push time; re-pushed on source image update.

Options considered

Option A (pure iCal fields only) was the current implementation. It was rejected because: citizen-facing views were text-only with no images or structured location links; source lookup required fragile uid-prefix parsing; and deduplication on upsert was unreliable. Option B (iCal core + capped extensions) was adopted. The iCal contract is fully preserved — extended fields are invisible to Outlook and any standard calendar client. The scope of extensions is capped to the fields above; adding more requires revisiting this section. Migration from Option A adds sourceType, sourceId, facilitySpaceId, categoryId, and imageUrl columns and updates the program sync to populate them.

What Feeds a Calendar and How

Program Sessions Program Session

A Program has one or more Sessions. A Session is the time-bound record (start, end, recurrence rule, exceptions). Sessions — not the Program itself — are the unit that maps to a calendar entry.

Push model (design target): When a Program is published or a Session is added/updated/deleted, a calendar entry is written immediately — not via a scheduled job. Staff should be able to see the entry on the website within seconds of publishing. sourceType = "programSession" and sourceId = session.id allow reliable upsert and deletion.

Calendar mapping: All program sessions push to the single Calendar that represents their audience (e.g. "Parks Programs", "Library Programs"). Sub-categorisation within that calendar uses the event-level categoryId field — not separate child calendars. See the Category & Tagging section below.

Board Meetings Board Meeting

The Boards module manages committees, agendas, and meeting schedules. Scheduled meetings push into the City Meetings calendar using the same push model as Programs. sourceType = "boardMeeting". The Board module owns the meeting record; the Calendar is the publication surface. This is a design target — Boards integration with Calendar is not yet implemented.

City Holidays Holiday

Staff-authored native events on the City Holidays calendar. sourceType = "native". Future enhancement: when a holiday entry is saved, affected Facility records are offered an exception-hours update. See the Holiday → Facility hours future design note above.

Reservations Reservation

A confirmed reservation blocks a FacilitySpace for a time window. It surfaces on the facility's calendar as an entry with a limited citizen-visible payload: space name, date/time, "Reserved" label. No identifying information about the reserving party is shown publicly. Staff-facing views (admin portal, Exchange) carry the full reservation record. See Reservations.

External ICS Feeds ICS Import

External webcal/ICS sources are polled on a schedule and cached as CalendarEvent rows. sourceType = "icsFeed". MyGov caches the entries — the external source remains authoritative. Staff cannot edit imported entries; changes must be made at the source.

Category & Tagging

Decision: event-level tags; Calendar as subscription audience

The two models considered were calendar-as-category (each category maps to its own Calendar, rollup via parent/child hierarchy) and event-level tags (one Calendar per subscription audience; a categoryId on the event for filtering and display).

Event-level tags is the adopted approach. The separation is:

  • Calendar = subscription audience. Who would subscribe to this as a feed? "Parks Programs" and "Library Programs" are distinct audiences. "Parks Athletics" is not — it is a filter within Parks Programs.
  • CategoryId = content filter within a subscription. Sub-categorisation, display colour/icon, and UI filtering use the event tag, not a separate calendar object.

Why not calendar-per-category

The Microsoft Ecosystem angle is decisive. Pushing category-level distinctions via separate Outlook calendar subscriptions requires a staff member to subscribe to N calendars (one per Parks program category, potentially 50+) to get full visibility. Managing Exchange-backed resource mailbox access across N calendars multiplies the administrative surface unnecessarily. Moving a program to a different category would require migrating its events to a different calendar.

iCal's CATEGORIES field (populated from categoryId label at export time) provides category metadata to clients that honour it — Apple Calendar renders it; Outlook ignores it gracefully. We do not try to push shared colour-coding via ICS because no standard reliable mechanism exists for that. Staff who want to filter their Outlook subscription by category use Outlook's own search/filter tools.

Implications for CalendarCategory

data.CalendarCategory is a simple lookup table: id, label, colour, iconSlug. It is not a parent of Calendar — it is a property of CalendarEvent. Programs' ProgramCategory provides the categoryId value when pushing sessions; the Calendar layer owns the CalendarCategory record and the display contract. See Programs & Registration for how ProgramCategory maps to CalendarCategory.

Duplicate event risk

With event-level tags there is no parent/child calendar rollup — no event duplication risk. One event record; filtered at query/render time. The parent/child calendar hierarchy (see below) applies only to audience-level groupings (Parks Programs may have a parent "All Programs" for a combined city calendar view), not to category-level groupings.

Calendar Hierarchy and Rollup

Calendars support a two-level hierarchy at the audience level only — not at the category level (see Category & Tagging above). A parent aggregates entries from its children at display time; entries are not duplicated in the database. A child belongs to at most one parent.

Example hierarchy:

  • All Programs (optional top-level parent)
    • Parks Programs
    • Library Programs
  • Parks Programs — flat; events carry categoryId for sub-filtering (Athletics, Aquatics, Camps, etc.)
  • Library Programs — flat; events carry categoryId
  • City Meetings — flat; receives Board Meeting pushes
  • City Holidays — flat; staff-authored; future facility hours linkage
  • Auburn Recreation Center (parent, by facility)
    • Main Gym (space calendar)
    • Meeting Room A (space calendar)
Current state: data.Calendar has no parentCalendarId field. The hierarchy is a design target — the schema is currently flat (single-level). Parent/child rollup requires adding the FK and updating the calendar UI component to union child entries at render time.

Sync Approach

The guiding principle: an employee who publishes a record should be able to verify it is live on the website immediately. Async sync to external systems (Outlook, webcal subscribers) is acceptable, but a manual trigger must always be available.

Internal writes — on-publish, synchronous. When a Program Session is published or a native Calendar Event is saved, the calendar entry is written in the same request. The website reflects the change before the employee navigates away. This applies to inserts, updates, and deletes. Currently implemented as a manual POST endpoint — on-publish auto-trigger is the remaining gap.
Microsoft 365 sync — immediate write. MyGov writes to Exchange immediately when any calendar event or booking is created, updated, or deleted — not on a scheduled job. For shared calendars, the write happens in the same request as the MyGov save when Calendar.exchangeCalendarAddress is set. For resource mailboxes, the Exchange event is written at booking submission (blocking the room immediately) regardless of approval state. There is no polling or scheduled sync for the write direction.
External ICS pull — scheduled refresh. External calendar feeds are refreshed on a configurable interval per Calendar record. A manual refresh action should be available for imported calendars. Known gap: the current refresh is upsert-only — events removed from the external feed are never deleted.

Microsoft 365 and Outlook

Calendar as Exchange broker

The Calendar layer is the single integration point between MyGov and Exchange. Modules (Reservations, Programs) do not call Exchange directly — they push to a Calendar, and the Calendar controller decides whether that also means writing to Exchange. This is the broker model.

Two fields on data.Calendar implement this:

FieldTypeNotes
exchangeCalendarAddressstring?The SMTP address of the Exchange calendar or shared mailbox this Calendar syncs events to/from. Null = no Exchange sync. Used with Calendars.ReadWrite Graph API permissions. Distinct from FacilitySpace.exchangeResourceMailbox — see below.
isPublicboolDefault true. When false, no ICS feed is published and the calendar is not surfaced on the public website. The calendar still exists as an internal availability source. Use for conference rooms and staff-only spaces.

Note on two Exchange fields: FacilitySpace.exchangeResourceMailbox and Calendar.exchangeCalendarAddress may reference the same SMTP address for a reservable conference room, but they represent different Exchange concepts and different Graph API operations. The space field tracks the room as an Exchange Place (room finder, meeting invite resource — Place.ReadWrite.All). The Calendar field tracks it as an Exchange calendar to sync events to (Calendars.ReadWrite). A public programs calendar like "Parks Programs" has a Calendar.exchangeCalendarAddress pointing to a shared mailbox but no corresponding FacilitySpace.exchangeResourceMailbox. See Microsoft Platform.

Why broker and not direct

Two alternatives were considered and rejected:

  • Module-direct Exchange access — Reservations calls Exchange via MSGraphHandler independently of the Calendar layer. Rejected because it creates two parallel time-management tracks for a space: CalendarEvent rows (Programs) and Exchange events (Reservations). Availability queries must union both sources. Adding a new module that needs Exchange would require its own Exchange integration.
  • No Calendar for reservations — Exchange-only for reservable spaces; Calendar layer not involved. Rejected because it splits the availability surface: Programs push to Calendar; Reservations go straight to Exchange. A conflict check (can we book this room at 2pm?) requires querying two separate systems and reconciling them. The broker model gives a single authoritative source.

Calendar schema for FacilitySpaces

Each FacilitySpace that participates in time-based activities (Programs, Reservations, or both) has a corresponding data.Calendar record. The FK is Calendar.facilitySpaceId (or via the parent Facility for facility-level calendars). Configuration:

  • Conference room (staff-booked via Outlook): exchangeCalendarAddress set to the room mailbox address; isPublic = false. Staff subscribe in Outlook; no public ICS feed. The room also has FacilitySpace.exchangeResourceMailbox set (the Place/resource identity). Both fields may reference the same mailbox — they serve different Graph API operations.
  • Reservable public space (pavilion, study room): exchangeCalendarAddress null; isPublic = true. Availability managed entirely within MyGov; public ICS feed shows upcoming bookings with a limited payload ("Reserved").
  • Program space (gym, pool): exchangeCalendarAddress optionally set if staff also use Outlook to view the schedule; isPublic = true. Program sessions appear on the public calendar and optionally sync to the Exchange calendar.

Outlook calendar subscriptions (webcal / ICS)

When isPublic = true, MyGov publishes the calendar as an ICS feed. Staff and citizens can subscribe in Outlook or any calendar client. MyGov is the source of truth; the ICS feed is read-only. Staff adding events directly to their Outlook subscription does not write back to MyGov — this workflow should be explicitly discouraged in staff documentation and onboarding. Changes must be made in the MyGov admin portal.

When isPublic = false, only Outlook access via the Exchange resource mailbox is available. Staff subscribe to the mailbox as a shared calendar in Outlook and see events with full detail. No ICS endpoint is exposed.

Coordinator attendee communication

When a Program coordinator needs to email all registrants with updates, the Exchange resource mailbox reply-all is not a reliable primary channel (depends on how invitations were issued). The designed pattern is a "notify attendees" action in the admin portal using eNotifier or a direct MSGraph SendMail — not the mailbox. See Distribution & Publishing.

Audit Findings

Codebase audited: api/Models/organization/Calendar.cs, api/Models/organization/CalendarEvent.cs, api/Models/CMS/ProgramCategory.cs, api/Models/CMS/ProgramSession.cs, api/Controllers/cityPrograms/CalendarController.cs, my/js/modules/events/

FeatureStatusNotes
External ICS feed pull + cacheBUILTManual trigger; no scheduled refresh job wired yet. Upsert-only — stale entries not deleted.
Program session → CalendarEvent pushPARTIALEndpoint built and working (POST /programs/{id}/sync-calendar); manual trigger only — on-publish auto-trigger not implemented.
Calendar broker fields — exchangeCalendarAddress, isPublicNOT STARTEDNeither field exists on data.Calendar today. exchangeCalendarAddress is a new field (distinct from FacilitySpace.exchangeResourceMailbox — see Microsoft Platform doc). isPublic migrates from cms.FacilitySpaceContact.isPublic for space calendars.
Program session → Exchange blocking (via Calendar broker)NOT STARTEDPhase 2 of sync-calendar. Depends on Calendar broker fields above. Calendar controller writes Exchange event when Calendar.exchangeCalendarAddress is set.
ICS feed export (outbound)BUILTPer-facility feed at GET /calendars/facility/{id}/feed.ics with date filter.
Option B migration — extension fieldsNOT STARTEDSchema is Option A today. Migration adds: sourceType, sourceId, facilitySpaceId, categoryId, imageUrl columns. Update program sync to populate all five on push.
Parent/child calendar hierarchyNOT STARTEDNo parentCalendarId on Calendar model. Single-level only.
On-publish auto-sync triggerNOT STARTEDProgram publish does not call sync-calendar automatically.
Admin UI for refresh / sync triggerNOT STARTEDEvents module has basic CRUD only — no refresh or sync buttons.
Boards → City Meetings pushNOT STARTEDBoards module not yet integrated with Calendar layer. Data model for the push (Meeting entity, FacilitySpaceId, agenda visibility) is a Boards module design question — does not affect the Calendar layer.
Holiday → Facility exception hoursNOT STARTEDDesign target only. Requires typed exception model on FacilityContact.hours. Rollout scope (all facilities, opt-in, by type) and confirmation flow are implementation decisions for the Holidays/Calendar module build.
Stale event deletion from ICS pullNOT STARTEDRefresh endpoint is upsert-only; events removed from source feed are never deleted.