Skip to content

0012 — Announcements (one-way broadcast) ​

Status: Accepted (2026-06-18 — owner accepted during ADR review)

Date: 2026-06-17

ADO work item: AB#3089

Deciders: Kristopher Turner (platform owner)


Context ​

The platform strategy identifies Announcements as a core feature of the community portal (Epic AB#3075). Heritage Virginia is a closed, member-only community; the communication model is intentionally structured, not conversational. Announcements are the primary mechanism by which ministry leadership communicates to the congregation, a small group, or a role-scoped audience.

ADR 0007 already defines the Announcements table (one-way, no replies) as part of the data model. ADR 0006 defines the five RBAC roles (Administrator, Minister, Community Leader, Small Group Leader, Member) and the approval authority each carries. ADR 0008 establishes that every feature is a slice of the platform API consumed through api-client; Announcements is no exception.

This ADR covers the content artifact layer of the three-layer communications model:

LayerADRConcern
Content artifact0012 (this ADR)Announcements data model — fields, audience scope, priority, status values, receipts
Authoring & approval workflowADR 0023Who drafts vs who approves; the comms_author role; queue lifecycle; self-approve prohibition
Delivery transportADR 0013Channel adapters, fan-out, retry, member preferences

The decisions this ADR must record are:

  1. Where announcement content is stored and what metadata it carries.
  2. What the valid status values are and which transitions are legal (workflow detail and approval authority are delegated to ADR 0023).
  3. How delivery to recipients is handed off without inventing a bespoke transport.
  4. What the boundary is between Announcements (content artifact) and the notifications transport (ADR 0013).
  5. Whether read/acknowledged state is tracked per member.

The constraint that drives every other choice: Announcements are one-way. Recipients cannot reply. There are no user-to-user threads on an announcement. This is not a gap to fill later; it is the defining characteristic of the feature and the reason no separate content moderation system is needed — approval authority is already encoded in RBAC.

Decision ​

We will store announcements in the Announcements table (ADR 0007) with audience scoping by RBAC role and/or family group, optional priority level, and optional scheduled publish and expiry timestamps; authoring and approval are gated to approver-capable roles (Administrator, Minister, Community Leader, or Small Group Leader) enforced server-side per ADR 0006; delivery fans out through the platform notifications transport (ADR 0013) rather than a bespoke per-channel path; and per-member read/acknowledged state is tracked in a companion AnnouncementReceipts table. Recipients have no reply capability at any layer.

Data model ​

The Announcements table (established in ADR 0007) is extended with the following columns to support this feature. No schema changes that contradict ADR 0007 are introduced.

text
Announcements
─────────────────────────────────────────────────────────────────────────
id                 UUID PK
author_user_id     FK → Users.id          (who wrote it)
approved_by_id     FK → Users.id NULL     (who approved; must differ from
                                           author_user_id per ADR 0023 when
                                           author holds comms_author role)
title              NVARCHAR(255)
body               NVARCHAR(MAX)          (rich text; server-side sanitized)
priority           TINYINT                (0 = normal, 1 = important,
                                           2 = urgent)
audience_role      TINYINT NULL           (targets a specific RBAC role;
                                           NULL = all members)
audience_group_id  FK → FamilyGroups.id NULL  (targets a specific group)
status             VARCHAR(20)            (draft | pending_approval |
                                           approved | rejected | expired)
scheduled_at       DATETIMEOFFSET NULL    (NULL = publish immediately on
                                           approval)
expires_at         DATETIMEOFFSET NULL    (NULL = no expiry)
created_at         DATETIMEOFFSET
updated_at         DATETIMEOFFSET

AnnouncementReceipts
─────────────────────────────────────────────────────────────────────────
id                 UUID PK
announcement_id    FK → Announcements.id
user_id            FK → Users.id
delivered_at       DATETIMEOFFSET NULL
read_at            DATETIMEOFFSET NULL    (NULL = delivered but not read)
acknowledged_at    DATETIMEOFFSET NULL    (NULL = no explicit ack required)

Audience scoping is evaluated server-side at delivery fan-out time; the API never exposes announcement content to a member whose role/group is outside the target audience.

Authoring and approval workflow boundary ​

The status field captures workflow state: draft | pending_approval | approved | rejected | expired. The API enforces valid state transitions; clients reflect state — they do not drive it.

Who can author, who can approve, how the queue operates, and the prohibition on self-approval are defined in ADR 0023. This ADR records only the status values and the data columns (author_user_id, approved_by_id) that the workflow operates on.

Key constraint from ADR 0023: approved_by_id must never equal author_user_id when the drafter holds the comms_author role. That check is enforced server-side at the approval endpoint; the schema permits NULL in approved_by_id until approval occurs.

Approval collapses moderation. Because only approver-capable roles can publish, there is no unapproved user-generated content reaching members. A separate content moderation system is explicitly out of scope.

Delivery ​

On a status transition to approved (or when scheduled_at arrives), the API calls the platform notifications transport (ADR 0013). The transport resolves the recipient list from the audience scope and delivers through whichever channels are active (email, SMS, in-app, push). The Announcements feature does not own any channel adapter; it calls one service method on the transport.

This boundary is important:

ConcernOwner
Announcement content, metadata, approval workflowAnnouncements feature (this ADR)
Channel adapters (email, SMS, in-app, push)Platform notifications transport (ADR 0013)
Member channel preferencesPlatform notifications transport (ADR 0013)
Delivery fan-out and retryPlatform notifications transport (ADR 0013)
Receipt recording (AnnouncementReceipts)Announcements feature (this ADR)

The Announcements API writes a receipt row when the transport confirms delivery and updates read_at when the member opens the announcement in-app.

One-way enforcement ​

No API endpoint accepts a reply to an announcement. The data model has no reply or comment table linked to Announcements. The in-app UI renders a read-only view; there is no reply affordance. This constraint is architectural, not a UI configuration toggle.

Alternatives considered ​

OptionProsConsWhy not chosen
One-way broadcast stored in Announcements table, delivered via platform transport (chosen)Reuses ADR 0007 schema; approval = moderation; no bespoke delivery; thin client; single receipt storeRequires platform notifications transport (ADR 0013) to exist before Announcements can deliver— chosen
Two-way: allow member replies / comments on announcementsEngagement signal; members feel heardRe-introduces moderation burden; requires threading; contradicts the closed one-way model; out of scope per platform strategyRejected — explicitly contradicts the one-way constraint and the no-separate-moderation-system goal
Third-party mailing-list tool (Mailchimp, Constant Contact, etc.)Familiar UX; delivery infrastructure managed externallyBreaks RBAC audience scoping; no in-app delivery; member data leaves the platform; cost; no receipt visibility inside the appRejected — RBAC scoping and in-app delivery cannot be satisfied outside the platform
Bespoke per-channel delivery inside this featureNo dependency on ADR 0013 transportDuplicates channel adapters, retry logic, and member preferences that the platform transport already owns; maintenance burden multiplied by every future feature that needs deliveryRejected — platform composition rule (ADR 0008): features call shared services, they do not re-implement them
No approval step — any approver-role user publishes immediatelySimpler workflowNo audit trail of who reviewed content; minister loses oversight of community/small-group announcementsRejected — approval authority per RBAC role is the moderation mechanism; removing it removes the safety guarantee

Consequences ​

Positive ​

  • Approval authority encoded in RBAC (ADR 0006) eliminates the need for a separate content moderation pipeline; the minister/administrator always has final say before anything reaches members.
  • One-way enforcement is architectural (no reply endpoint, no reply table, no reply UI) — it cannot be accidentally enabled by a configuration change.
  • Delivery reuses the platform notifications transport (ADR 0013); Announcements gains all future channel improvements (e.g. adding WhatsApp) without any change to this feature.
  • Audience scoping by RBAC role and/or family group means a small-group leader reaches only their group; a minister reaches all members — enforced server-side, not by client convention.
  • AnnouncementReceipts gives leadership visibility into who has read a given announcement, which is useful for time-sensitive communications (e.g. a weather cancellation).

Negative / trade-offs ​

  • Delivery dependency. Announcements cannot fan out until the platform notifications transport (ADR 0013) is implemented. In-app display of announcements can ship independently; email/SMS/push delivery requires the transport.
  • Receipt volume. AnnouncementReceipts grows at O(announcements × members-in-scope). For a congregation of typical size (< 500 members) this is negligible; it should be noted for future archiving policy.
  • The scheduled/expiry clock logic requires the API to have a lightweight scheduler or a recurring Azure Functions timer trigger. This is a platform-level concern; the Announcements feature defines the requirement; the platform team implements the timer.

Risks &ZeroWidthSpace;

  • Audience scope misconfiguration — an author targets audience_role = NULL (all members) when they intended a specific group. Mitigation: the approval step provides a second reviewer; the UI must display a clear audience summary before the approver confirms.
  • Transport unavailability — if ADR 0013 transport is not ready when Announcements ships, in-app delivery works but push/email/SMS do not. Mitigation: design the fan-out call as fire-and-notify; degrade gracefully; document the dependency in the release plan.
  • Receipt table bloat — long-lived deployments with high announcement frequency. Mitigation: define a retention/archive policy before Phase 2 launch; expired announcements and their receipts can be cold-archived after expires_at + 90 days.

References &ZeroWidthSpace;

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.