Skip to content

0023 — Communications authoring & approval workflow ​

Status: Accepted (2026-06-18 — owner approved the sixth comms_author role; ADR 0006 role table updated accordingly)

Date: 2026-06-18

ADO work item: AB#3089

Deciders: Kristopher Turner (platform owner)


Context ​

One-way broadcast — governing constraint ​

The Messages feature (and the overlapping Announcements feature) is a one-way broadcast system, not a two-way chat or messaging product. There is no member-to-member reply, thread, or direct message capability — this is an explicit, locked platform constraint (see ADR 0013 Alternatives, "Two-way chat / direct messages between members": rejected). Broadcast communications flow in one direction: from an authorized author, through a minister's approval queue, out to members via SMS, email, in-app notification, and push notification.

Authoring rights are held by a defined set of roles:

  • Small group ministers / group leaders — post updates, schedules, and information to their group's members
  • Homeschool teachers and co-op coordinators — post to homeschool families enrolled in their program
  • Ministers and ministry leaders — post to their ministry audience or the whole community
  • Event organizers (comms_author role delegated for an event scope) — post to registered event participants

Members receive messages. Members do not reply, react, or initiate broadcasts.

Note — potential future convergence: The platform currently carries two related broadcast surfaces: Announcements (ADR 0012, church-wide from leadership) and Messages (targeted broadcasts from small group leaders, teachers, and event organizers). These may converge into a single unified broadcast system in a future revision. The authoring and approval workflow defined in this ADR is designed to be content-type-agnostic and would apply without change to a merged system.


This ADR covers the authoring and approval workflow layer of the three-layer communications model:

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

ADR 0012 defines the Announcements table schema, the draft → pending_approval → approved | rejected status lifecycle, and the handoff to the platform notifications transport (ADR 0013). What ADR 0012 leaves underspecified is who drafts versus who approves, and whether those can be the same person for all use cases the ministry requires.

Heritage Virginia delegates writing tasks. A minister may ask a volunteer or a ministry coordinator to draft announcements for the whole congregation, for a specific small group, or for a specific ministry — without granting that person the authority to publish unilaterally. The real-world workflow has two distinct actors:

  1. An author — tasked to draft; has no right to approve their own content.
  2. An approver — a minister or community leader who reviews the queue and approves or rejects before anything reaches members.

The five canonical RBAC roles in ADR 0006 (admin, ministry_leader, group_leader, member, visitor) do not accommodate this separation cleanly. Every role that can currently write announcements (admin, ministry_leader, group_leader) also carries implicit approval authority under the ADR 0012 model. There is no way, within the current five-role set, to grant someone authoring access for community-wide or cross-ministry communications without also granting them approval authority. This creates two problems:

  • Self-approval risk. A small group leader who is tasked to draft a community-wide announcement can submit and approve it themselves — removing the oversight the minister intends.
  • Delegation without escalation. There is no clean way to say "this person can draft for any audience but cannot publish without a minister's sign-off."

ADR 0008 establishes that every surface hits the same API endpoints; there is no separate authoring system. Drafting on a phone and approving on the web must follow the same state machine, enforced server-side. ADR 0005 requires that every author/approve/publish action is logged in the AuditLog table.

This ADR proposes the workflow model that satisfies real-world ministry delegation, closes the self-approval gap in ADR 0012, and records a proposed extension to ADR 0006.

Decision ​

We will introduce a sixth application RBAC role, Communications Author (slug: comms_author), whose scope is strictly write-without-approve: a Communications Author may create and edit Announcements drafts for any audience they are explicitly assigned (whole community, a specific ministry, a specific small group), may submit drafts to the approval queue, but cannot approve, reject, or publish any announcement — including their own. Approval authority remains exclusively with ministry_leader and admin roles, enforced server-side per ADR 0006. Every state transition (draft → pending_approval → approved | rejected → published) is logged in the platform audit log per ADR 0005 and is reachable from any platform surface (mobile or web) through the typed api-client SDK per ADR 0008.

The two-actor separation mapped onto the ADR 0012 lifecycle ​

text
  AUTHOR (comms_author role)           APPROVER (ministry_leader | admin role)
  ─────────────────────────            ───────────────────────────────────────
  Creates draft                        ← queue visible in Admin / Ministry portal
  Sets title, body, audience,          Reviews content + audience scope
    priority, scheduled_at             Approves → status: approved
  Submits for approval                             → notifications transport fans out
    → status: pending_approval                     → AnnouncementReceipts written
                                       Rejects → status: rejected
                                                 → author notified
  Revises and resubmits if rejected
    → status: draft → pending_approval

Audience scope columns (audience_role, audience_group_id) on the Announcements table (ADR 0012) determine who can receive a given announcement. The API additionally gates what audiences a comms_author may write for:

comms_author assignmentMay author announcements targeting
audience = communityAll members (audience_role = NULL, audience_group_id = NULL)
audience = ministry:<id>That ministry's role scope
audience = group:<id>That specific FamilyGroups entry

An admin sets these assignments on the comms_author row; the API rejects draft creation that exceeds the author's assigned audience scope. This assignment is stored in a lightweight UserCommunicationsScope join table (one row per author per permitted audience).

State machine (extends ADR 0012) &ZeroWidthSpace;

text
  draft  ──[author submits]──►  pending_approval
    ▲                                 │
    │   [approver rejects]            │  [approver approves]
    └─────────────────────────        ▼
                               approved ──[scheduled_at or immediate]──► published


                                                                   notifications transport
                                                                       (ADR 0013)

expired is a terminal state reachable from approved/published when expires_at passes (unchanged from ADR 0012).

API-first: cross-surface by design &ZeroWidthSpace;

Because every surface calls the same API endpoints (ADR 0008), the authoring and approval workflow is surface-agnostic:

  • A comms_author drafts on their phone via apps/mobileapi-clientPOST /announcements (status: draft).
  • A minister opens apps/web (or the admin portal) → api-clientGET /announcements?status=pending_approval → reviews → PATCH /announcements/:id/approve.
  • The API validates the role on every call; the surface receives only the response.

No announcement logic lives in the client. The queue is the same data regardless of which surface the approver uses.

Audit &ZeroWidthSpace;

Every state transition writes an AuditLog row per ADR 0005:

Eventactor_user_idtarget_typedetail
announcement.draft_createdauthorAnnouncementaudience scope
announcement.submittedauthorAnnouncementfrom draft
announcement.approvedapproverAnnouncementapproved_by_id set
announcement.rejectedapproverAnnouncementrejection reason
announcement.publishedsystem/approverAnnouncementfan-out initiated

author_user_id and approved_by_id on the Announcements row (defined in ADR 0012) are always distinct when a comms_author is the drafter — enforced by the API: the endpoint that transitions pending_approval → approved rejects the request if the caller's user_id matches author_user_id.

Relationship to ADR 0006 &ZeroWidthSpace;

This ADR proposes an extension to ADR 0006. The comms_author slug is a sixth Plane-2 role. On acceptance of this ADR, ADR 0006 should be updated to add the row to the canonical role table, or a superseding ADR should be issued. The existing five roles and their enforcement rules are unchanged; comms_author adds a write-without-approve permission tier that did not previously exist.

Alternatives considered &ZeroWidthSpace;

OptionProsConsWhy not chosen
Sixth role: comms_author — write without approve (chosen)Clean author ≠ approver separation; explicit in the role model; audience scope is assignable per author; no existing role semantics changedExtends ADR 0006 (requires update or supersession); adds a UserCommunicationsScope join table; adds one role to every authorization check path— chosen
Reuse existing roles — group_leader authors within their group; ministry_leader authors for their ministry; both self-approveNo schema or role model changeLoses the author ≠ approver guarantee entirely; a group leader or ministry leader can publish without review; minister loses oversight of community-wide communications authored by delegated writers; does not model the "write for whole community, cannot approve" use caseRejected — self-approval is the specific failure mode this ADR exists to prevent
Separate CMS / communications tool (e.g. Mailchimp, a headless CMS)Off-the-shelf authoring UX; no in-house queue to buildMember data leaves the platform; RBAC audience scoping cannot be enforced externally; no in-app delivery or receipt tracking; recurring cost; contradicts the one-platform principle; approval workflow would need to be rebuilt or approximated in a third-party toolRejected — cost, data residency, and platform composition rules (ADR 0008) all prohibit it

Consequences &ZeroWidthSpace;

Positive &ZeroWidthSpace;

  • The minister retains full oversight: no announcement reaches members without an admin or ministry_leader explicitly approving it, regardless of who drafted it.
  • Authors can work from any surface at any time; the queue appears to approvers on any surface — the API-first design (ADR 0008) makes this free.
  • The comms_author role is additive. No existing role permissions change; existing users are unaffected.
  • Delegating writing without delegating publishing authority is a real need for growing ministries; this model supports it without privileged workarounds.
  • Every action is auditable (ADR 0005); author_user_idapproved_by_id is enforced at the API layer, not just by convention.
  • Moderation collapses into the approval workflow (unchanged from ADR 0012): only approved content ever reaches members; no separate moderation pipeline is needed.

Negative / trade-offs &ZeroWidthSpace;

  • ADR 0006 must be updated (or a superseding ADR issued) to record the sixth role. This is coordination overhead, not implementation complexity.
  • A new join table (UserCommunicationsScope) is required to store per-author audience assignments. It is a small table (one row per author per permitted scope) but it is an additional schema object to migrate.
  • Admin UI must expose controls for assigning audience scope to comms_author users. This is in scope for the Admin & Ministry Portal (ADR 0021) but adds a configuration screen that would not be needed if the simpler "reuse existing roles" approach were taken.
  • The approved_by_id ≠ author_user_id server-side check must be added to the approval endpoint; without it the constraint is advisory only.

Risks &ZeroWidthSpace;

  • Scope misconfiguration — an admin assigns a comms_author a broader audience scope than intended (e.g. community-wide when only a group was meant). Mitigation: the admin UI displays the effective audience at assignment time; the approval step provides a second review of the targeted audience before publish.
  • Approval queue neglectpending_approval announcements age without review, causing time-sensitive content to miss its window. Mitigation: the platform notifications transport (ADR 0013) notifies approvers when a new submission enters the queue; a scheduled_at that lapses while still in pending_approval is flagged as overdue in the admin portal.
  • Role proliferation — adding a sixth role increases the surface area of every role-check in the API. Mitigation: the comms_author role is narrowly scoped to the Announcements feature only; it carries no permissions in any other feature area.
  • ADR 0006 drift — if ADR 0006 is not updated promptly after this ADR is accepted, the canonical role table becomes stale. Mitigation: acceptance of this ADR creates an ADO task to update ADR 0006 before any implementation begins.

References &ZeroWidthSpace;

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