Layer
Distribution & Publishing
The presentation layer. Where and how records become visible — to the public via the CMS/WebPages, to subscribers via eNotifier, and to developers via embeddable web components. Every module has operational data; this layer decides which surfaces that data reaches and what form it takes when it gets there.
| Status | Draft |
| Last updated | — |
| Source docs | Consolidated from contact-surface.html and enotifier.html (now redirects) |
| Related |
Layer Location · Calendar & Time Platform Workflow Engine · Microsoft Platform Module Reservations · Programs & Registration · Forms |
Scope of This Layer
Distribution and Publishing is the answer to: "I have data in the system — how does it become visible or send a notification?" The question applies independently of which module owns the data. A Parks program, a reservation confirmation, a job posting, and a board meeting agenda all need to be published somewhere. This layer defines how that happens without requiring each module to invent its own CMS integration.
The public website. CMS pages reference operational data via contact records (Org → CMS bridge). Staff control what appears and how via the CMS admin.
Email distribution to subscriber interest lists. Staff-composed bulk email with opt-in push automation for transactional notifications.
Embeddable JavaScript widgets that surface live MyGov data (calendars, program lists, news feeds) on any page — internal or external.
System-triggered single-recipient emails: reservation confirmations, registration receipts, form submission acknowledgments. Delivered via Workflow Engine / MSGraph.
Org → CMS Bridge: The Entity / Contact Pattern
The operational layer (organization schema) holds the source of truth for departments, facilities, spaces, programs, and staff. The CMS layer (cms schema) holds what is displayed to the public. These are deliberately separate — operational data changes often; public presentation changes less often and needs editorial control.
The bridge is a Contact record: a CMS-layer entity (e.g.
FacilityContact) that carries display-layer overrides of its operational
source (e.g. Facility). A COALESCE-style pattern resolves the
display value: COALESCE(contact.nameOverride, facility.name) —
the override wins if set, otherwise the operational field feeds through.
This "seed and override" approach means:
- New entities get a Contact record automatically (seeded from operational data).
- Staff never have to re-enter data they already entered in the operational module.
- CMS display can diverge from operational data intentionally (marketing copy, hours notes) without corrupting the source record.
- The operational record remains clean and queryable; CMS fields do not leak into it.
Pattern Coverage
| Org entity | CMS contact | Pattern |
|---|---|---|
organization.Department | cms.DepartmentContact | 1-to-1, seed on create |
organization.Employee | cms.EmployeeContact | 1-to-1, opt-in (not all staff are public) |
organization.Facility | cms.FacilityContact | 1-to-1, seed on create |
organization.FacilitySpace | cms.FacilitySpaceContact | 1-to-1, seed on create; display only |
data.Program | cms.ProgramContact | 1-to-1, seed on publish |
data.News | (native CMS — no bridge needed) | CMS-native; no operational source |
FacilitySpace — Three-Entity Model
FacilitySpace is the only entity with three layers: spatial identity,
public display, and booking policy. Each belongs to its own record.
| Entity | Schema | Purpose | Key fields |
|---|---|---|---|
| FacilitySpace | organization | Spatial identity — what the space is, canonically | name, capacity, facilityId, ExchangeResourceMailbox ← MISLOCATED; belongs on data.Calendar |
| FacilitySpaceContact | cms | Public display — what the space looks like on the website | nameOverride, description, amenities, photos, isReservable (discovery flag), bookingRules ← MISLOCATED; belongs on ReservationConfig |
| FacilitySpaceReservationConfig | organization | Design target Booking policy — how reservations of this space work | FacilitySpaceId, RequiresApproval, RequiresLibraryCard, RequiresPayment, Cost, OperatingHours, MaxDurationMinutes, MaxLeadDays, BufferBefore/AfterMinutes, MaxConcurrentBookings, BlackoutDates, Notes |
| Calendar | data | Time view and Exchange broker for this space | facilitySpaceId (FK), exchangeResourceMailbox (nullable), isPublic (bool). See Calendar & Time. |
isReservable lives on FacilitySpaceContact as a display
and discovery flag — "should a 'Reserve this space' button appear on the website?"
This is distinct from whether the space has a ReservationConfig record
(the operational flag). Both must be true for citizen-facing booking to work.
ExchangeResourceMailbox is opt-in per space and lives on
data.Calendar (not on FacilitySpace or FacilitySpaceReservationConfig).
The Calendar is the Exchange broker — modules push to the Calendar; the Calendar
controller handles Exchange sync where exchangeResourceMailbox is set.
See Calendar & Time.
WebPages / CMS Channel
The CMS module manages the public website's page tree — navigation, content blocks, embeds, and media. It is the primary channel for non-transactional, staff-authored content.
CMS pages do not store operational data directly. They reference it via Contact records (see above), web component embeds (see below), or explicit FKs in CMS page metadata. This allows staff to control the public presentation of a facility, program, or department without touching the operational record.
Open design question: CMS ↔ Org linkage
A CMS page about a Department does not currently have a verified FK to
organization.Department. If a department is renamed or merged, the
CMS page continues to show the old data unless a staff member manually updates it.
The Contact pattern resolves this for structured entities (Facility, FacilitySpace,
Program) but the question for free-form CMS pages remains open: should CMS pages
carry an optional sourceType / sourceId pair to declare
"this page is about Department 12"?
eNotifier Channel
eNotifier manages email distribution to opt-in subscriber lists. It is not a transactional email system — it is a bulk communication channel for keeping subscribers informed about topics they care about.
Interest Lists and Subscribers
Citizens subscribe to Interest Lists. An Interest List is a named topic (e.g. "Job Openings", "Parks Programs Updates", "City Council Agendas"). Staff manage Interest Lists in the admin portal; citizens manage their own subscriptions on the public website.
Trigger Model: Pull vs. Auto-Push
There are two models for how eNotifier campaigns are created and sent:
A staff member opens eNotifier, selects an Interest List, composes a message (choosing content blocks from the system), and sends. The staff member controls timing, content, and which list receives it. This is the correct model for most communications — announcements, updates, newsletters. A rich multi-item content block editor (e.g. "add 3 programs to this email") eliminates the need to send one email per item.
A specific Interest List can be configured with an auto-push rule in the Workflow Engine: when trigger condition X occurs, compose template Y and send to list Z. Example: "When a Job Posting is published, send a notification to the 'Job Openings' list." This is opt-in per list, not the default. It is appropriate only when the trigger is reliable, the audience is genuinely interested in every item of this type, and the volume is low enough that subscribers won't feel spammed.
Why pull-default matters
The historical misuse pattern was: staff sent one email per program session because there was no way to batch multiple programs into a single email. Auto-push on program publish would have replicated this at system level — one email per publish event. The solution is a content block editor that lets staff select multiple items for a single email, making the pull model genuinely usable for batch communications. Auto-push should only be used where a 1:1 trigger-to-email ratio is actually correct (e.g. a single job posting going to job-seekers who want to know about every opening).
EnotifierEmployee — staff list hygiene
EnotifierEmployee records link Interest Lists to staff members. When a
campaign is sent to an Interest List, the linked employees also receive it. Currently
these records are not linked to organization.Employee records — they
are free-text email addresses stored separately. When a staff member leaves, their
EnotifierEmployee rows are not automatically removed.
EnotifierEmployee
should carry a FK to organization.Employee so that staff departures
automatically clean up list subscriptions. Address this before the next significant
staff turnover to prevent campaign leakage to inactive mailboxes.
Message assembly
Campaigns are composed from typed content blocks: announcements, program listings, event listings, news items, media items, and plain text. Each block type renders differently in the email template. The admin portal provides a multi-item block editor — staff should not be constrained to one item per block, and the block editor should make it trivially easy to add multiple items of the same type in one campaign.
Web Components Channel
MyGov exposes data through embeddable web components — custom HTML elements
(<auburn-calendar>, <auburn-programs>, etc.)
that can be dropped into any webpage and render live data from the MyGov API.
Components are defined in static/components/.
Components serve two audiences:
- Internal CMS pages — CMS page blocks can embed a component by name and configuration. This is how calendar views, program listings, and facility availability widgets appear on the public website without hand-coding them.
- External pages — Third-party sites (e.g. a partner organization's website) can include the component script tag and embed live MyGov data with a single line of HTML.
Components communicate with the MyGov API via standard GET requests. They are read-only from the component side — they display data, they do not write it. Forms embedded in pages are a separate mechanism (see Forms).
Transactional Email
Transactional emails are single-recipient, system-triggered messages: a reservation confirmation sent to the person who made the reservation, a form submission receipt, a registration confirmation. These are distinct from eNotifier bulk campaigns.
Transactional emails are sent via the Workflow Engine's MailHandler
or MSGraphHandler/SendTemplate action step. The Handlebars template
renders the message body using the form response or record data as context. A staff
member "notify attendees" action (e.g. from a Program detail page) is also
transactional — one email per attendee, not a bulk campaign. See
Workflow Engine for handler detail.
Email template management
Email templates are Handlebars files stored in the API codebase. They are not currently editable by staff in the admin portal — a code change is required to modify a transactional template. This is a known gap for operational flexibility. Whether templates should be admin-editable is an open design question.
Cross-Channel Design Questions
Should systems have a standard eNotifier integration point?
Currently, eNotifier integration is ad hoc — each module that wants to notify subscribers has to explicitly call the eNotifier API or configure Workflow Engine actions. Is there a standard "publishable" interface that any module can implement to opt into eNotifier distribution? This would reduce the per-module integration effort and make the Distribution layer a genuine platform feature rather than a series of one-off integrations.
Should CMS pages declare a sourceType / sourceId?
See the "CMS ↔ Org linkage" question above. The broader question is whether CMS pages should be first-class integration points — declaring what operational record they represent — or whether they remain fully editorial with Contact records as the only bridge.
Who owns the canonical public URL for a record?
A Facility has a CMS page, possibly a web component embed on another page, and
maybe a direct API endpoint link. Which URL is the "official" URL for a facility?
This matters for eNotifier deep links, CalendarEvent url fields, and
web component link targets. Currently there is no canonical URL registry — this is
resolved inconsistently across modules.
What is the distribution channel for Programs push?
When a new Parks program is published, who is notified and how? Currently: nothing automatic — staff must compose an eNotifier campaign. The auto-push model (Workflow Engine → eNotifier) would work for a "Parks Programs" interest list if the volume is right and subscribers genuinely want one email per program. A weekly digest approach (staff-composed, multi-program block) may be more appropriate. This is an open question that should be resolved with Parks staff before implementing any automation.
Audit Findings
| Finding | Priority | Detail |
|---|---|---|
EnotifierEmployee not linked to org.Employee |
MEDIUM-HIGH | Free-text email addresses. Staff departures do not clean up subscriptions. Add FK to organization.Employee; set ON DELETE CASCADE or soft-delete pattern. |
bookingRules on FacilitySpaceContact |
MEDIUM | Untyped JSON blob carrying booking policy on a display record. Migrate to FacilitySpace.reservationConfig JSON field (design target). Tracking in Reservations. |
ExchangeResourceMailbox on FacilitySpace |
MEDIUM | Field is correctly located on FacilitySpace (inventory association). No migration needed — previously noted as mislocated. See Location. |
| Email templates not admin-editable | LOW | Transactional templates require code changes. No timeline for admin template editor; accept current constraint until an explicit need drives the work. |
| No canonical public URL registry | LOW | CMS page URL for a Facility is not stored on the Facility record. Deep links in eNotifier and calendar entries are manually constructed. |
| Standard publishable interface — deferred design consideration | DEFERRED | Whether the platform should define a common interface for "publish to distribution channels" (rather than each module wiring its own eNotifier/Workflow actions) is a future architectural consideration. Does not block any current module build. |
| CMS page sourceType/sourceId FK — deferred design consideration | DEFERRED | Formally declaring which operational record a CMS page represents (via sourceType/sourceId) would enable automated deep-link resolution. Deferred until a concrete use case drives the work. |