Appearance
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:
| Layer | ADR | Concern |
|---|---|---|
| Content artifact | 0012 (this ADR) | Announcements data model — fields, audience scope, priority, status values, receipts |
| Authoring & approval workflow | ADR 0023 | Who drafts vs who approves; the comms_author role; queue lifecycle; self-approve prohibition |
| Delivery transport | ADR 0013 | Channel adapters, fan-out, retry, member preferences |
The decisions this ADR must record are:
- Where announcement content is stored and what metadata it carries.
- What the valid status values are and which transitions are legal (workflow detail and approval authority are delegated to ADR 0023).
- How delivery to recipients is handed off without inventing a bespoke transport.
- What the boundary is between Announcements (content artifact) and the notifications transport (ADR 0013).
- 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
Announcementstable (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 companionAnnouncementReceiptstable. 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:
| Concern | Owner |
|---|---|
| Announcement content, metadata, approval workflow | Announcements feature (this ADR) |
| Channel adapters (email, SMS, in-app, push) | Platform notifications transport (ADR 0013) |
| Member channel preferences | Platform notifications transport (ADR 0013) |
| Delivery fan-out and retry | Platform 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 ​
| Option | Pros | Cons | Why 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 store | Requires platform notifications transport (ADR 0013) to exist before Announcements can deliver | — chosen |
| Two-way: allow member replies / comments on announcements | Engagement signal; members feel heard | Re-introduces moderation burden; requires threading; contradicts the closed one-way model; out of scope per platform strategy | Rejected — 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 externally | Breaks RBAC audience scoping; no in-app delivery; member data leaves the platform; cost; no receipt visibility inside the app | Rejected — RBAC scoping and in-app delivery cannot be satisfied outside the platform |
| Bespoke per-channel delivery inside this feature | No dependency on ADR 0013 transport | Duplicates channel adapters, retry logic, and member preferences that the platform transport already owns; maintenance burden multiplied by every future feature that needs delivery | Rejected — platform composition rule (ADR 0008): features call shared services, they do not re-implement them |
| No approval step — any approver-role user publishes immediately | Simpler workflow | No audit trail of who reviewed content; minister loses oversight of community/small-group announcements | Rejected — 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.
AnnouncementReceiptsgives 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.
AnnouncementReceiptsgrows 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 ​
- 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 ​
- Platform strategy — Messaging: one-way broadcast
- ADR 0006 — Two-plane RBAC
- ADR 0007 — Account & Family-Group identity
- ADR 0008 — Platform composition
- ADR 0013 — Messaging & Notifications (transport) — the delivery layer; receives the fan-out call when an announcement is approved and published.
- ADR 0023 — Communications authoring & approval workflow — governs who drafts vs who approves, the
comms_authorrole, queue lifecycle, and the self-approve prohibition that applies toapproved_by_idon this table. - ADO: Epic AB#3075 (Announcements), Feature AB#3089 (this ADR and feature scope).