Skip to content

API Contracts — Heritage Community Hub ​

This document is the contract-level API specification for the Heritage Community Hub backend (apps/api). It is the authoritative reference for every surface (web, iOS, Android) and every feature team building on the platform.

Status: Living document. Contracts are additive across phases; breaking changes require an ADR update and a versioned migration path.

Source ADRs: 0003 (Clerk auth), 0006 (two-plane RBAC), 0007 (account/family/workflow), 0008 (platform composition), 0010 (Sermons & Music Hub), 0011 (Community Calendar), 0012 (Announcements), 0013 (Messaging & Notifications), 0021 (Admin & Ministry Portal), 0023 (Communications authoring & approval workflow).


1. API-First Principles ​

All access is through the typed api-client SDK over the backend API. No surface or feature talks to the database directly, self-authorizes, or holds business logic. This is the foundational rule from ADR 0008 — one place for all rules, one enforcement boundary.

Auth is Clerk-verified, identity is platform-owned. A caller presents a Clerk-issued JWT; the API validates it, resolves the Clerk sub claim to a Users row, and reads Users.role from the database for every authorization check. The client's role claim is never trusted. Children (parent-managed sub-accounts) authenticate through a separate credential path — they are not Clerk users (ADR 0003, 0007).

Authorization is server-side only. The six canonical roles (admin, ministry_leader, group_leader, comms_author, member, visitor) are enforced in the API on every protected endpoint (ADR 0006, extended by ADR 0023). A client UI may hint role-gated elements for UX convenience; the server is the authority.

Contracts live in packages/shared-types. Every request and response shape defined here has a corresponding TypeScript type in packages/shared-types. Web, mobile, and apps/api all import the same definitions — no ad-hoc inline types on any caller side.


2. Conventions ​

2.1 Base path ​

All endpoints are prefixed /api/v1. Example: GET /api/v1/me.

2.2 Versioning ​

The API is versioned by URL path segment (/v1). A breaking change increments the segment to /v2 and the previous version is maintained for at least one release cycle. Additive changes (new optional fields, new endpoints) do not require a version increment.

2.3 Authentication header ​

Every protected endpoint requires:

Authorization: Bearer <clerk-session-jwt>

Child (parent-managed) credential sessions present a platform-issued token in the same header format; the API distinguishes the credential type from the Users.credential_type column.

2.4 Error envelope &ZeroWidthSpace;

All error responses use a consistent envelope shape:

{
  "error": {
    "code":    string,   // machine-readable slug, e.g. "not_found", "forbidden", "validation_error"
    "message": string,   // human-readable summary
    "details": object?   // optional: field-level validation errors or structured context
  }
}

Standard HTTP status codes apply: 400 validation, 401 unauthenticated, 403 unauthorized, 404 not found, 409 conflict, 422 unprocessable, 429 rate limited, 500 server error.

2.5 Pagination &ZeroWidthSpace;

List endpoints that may return large result sets use cursor-based pagination:

GET /api/v1/<resource>?limit=<int>&cursor=<opaque-string>

Response:
{
  "data":       <array of items>,
  "pagination": {
    "nextCursor": string | null,   // null when no further pages
    "limit":      number
  }
}

Default limit is 20; maximum is 100. The cursor value is opaque — callers must not construct or modify it.

2.6 Validation &ZeroWidthSpace;

All request bodies are validated server-side. The api-client SDK applies the same schema definitions from packages/shared-types on the client before sending, as a convenience layer — not a security gate. The API rejects invalid payloads regardless of client-side validation state.

2.7 Timestamps &ZeroWidthSpace;

All timestamps are ISO 8601 UTC strings (2026-06-18T14:30:00Z). The client is responsible for formatting in the device locale.

2.8 Role slugs reference &ZeroWidthSpace;

RoleSlugMinimum for write operations
Community Leader / AdminadminYes
Ministry Leaderministry_leaderYes
Small Group Leadergroup_leaderYes
Communications Authorcomms_authorAnnouncements only — draft and submit, cannot approve
MembermemberRead-only for most resources
Visitor / ProspectivevisitorMinimal — signup flow only

Six canonical Plane-2 roles (ADR 0006, extended by ADR 0023). comms_author is narrowly scoped to the Announcements feature: holders may create and edit drafts for their assigned audience scope and submit for approval, but cannot approve, reject, or publish any announcement — including their own drafts. They carry no permissions outside Announcements.

visitor accounts are pending minister approval; they have read access only to resources explicitly documented as visitor-permitted below.


3. Auth & Session &ZeroWidthSpace;

Handles Clerk token exchange, current-user hydration, and sign-out. All other domains assume a successfully established platform session.

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/auth/sessionExchange Clerk JWT for a platform session; creates a Users row on first sign-in with role=visitor, status=pending_approvalPublic (Clerk JWT required){ clerkToken: string }SessionResponse
GET/api/v1/meReturn the authenticated caller's full profile and rolevisitorMeResponse
DELETE/api/v1/auth/sessionInvalidate the current platform session (sign-out)visitor204 No Content

SessionResponse (illustrative; to be detailed in shared-types):

{
  userId:      string (UUID),
  role:        RoleSlug,
  status:      "pending_approval" | "active" | "suspended",
  sessionToken: string   // short-lived platform session token
}

MeResponse — a User object (see Section 4) plus role, status, familyGroupId, and credentialType.

Notes:

  • On first sign-in, the API creates the Users row and triggers a notification to the minister approval queue (Section 5).
  • Child (parent-managed) sessions authenticate via POST /api/v1/auth/child-session (separate endpoint, distinct credential path; not routed through Clerk).
MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/auth/child-sessionAuthenticate a child sub-account using parent-managed credentialsPublic (platform username + PIN/password){ username: string, password: string }SessionResponse

4. Members & Profiles &ZeroWidthSpace;

The Users table is the single source of truth for all community members (ADR 0007). Directory access is scoped to approved members; visitors cannot browse the directory.

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/me/profileGet the caller's own profilevisitorUserProfile
PUT/api/v1/me/profileUpdate the caller's own profile (display name, photo, contact prefs)memberUpdateProfileRequestUserProfile
GET/api/v1/membersList approved members (directory)member?limit&cursor&q=<search>Paginated<MemberSummary>
GET/api/v1/members/:userIdGet a specific member's profilememberUserProfile
PUT/api/v1/members/:userIdAdmin update of any member's profile or roleadminAdminUpdateUserRequestUserProfile
DELETE/api/v1/members/:userIdDeactivate a member (cascades to child sub-accounts)admin204 No Content

UserProfile (illustrative):

{
  id:               string (UUID),
  displayName:      string,
  email:            string | null,   // null for child sub-accounts
  username:         string | null,   // parent-managed accounts only
  credentialType:   "social" | "parent-managed",
  role:             RoleSlug,
  status:           "pending_approval" | "active" | "suspended",
  familyGroupId:    string (UUID) | null,
  parentUserId:     string (UUID) | null,   // child sub-accounts only
  photoUrl:         string | null,
  createdAt:        string (ISO 8601)
}

MemberSummary — a condensed view (id, displayName, role, familyGroupId, photoUrl) used in directory listings.

Notes:

  • Members cannot modify their own role or status — those are admin-only fields.
  • Visitors see only their own profile via /me/profile; they cannot access /members.

5. Family Groups &ZeroWidthSpace;

Family Groups are the primary organizational unit (ADR 0007). Every approved member belongs to exactly one Family Group. Adding a spouse requires minister approval; adding a child sub-account is auto-approved (parent already vetted).

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/family-groupsCreate a new family group (triggered as part of new-member approval)adminCreateFamilyGroupRequestFamilyGroup
GET/api/v1/family-groupsList all family groupsministry_leader?limit&cursorPaginated<FamilyGroupSummary>
GET/api/v1/family-groups/:groupIdGet a family group and its membersmember (own group only; ministry_leader+ for any group)FamilyGroupDetail
PUT/api/v1/family-groups/:groupIdUpdate group name or primary memberadminUpdateFamilyGroupRequestFamilyGroup
POST/api/v1/family-groups/:groupId/spouseSubmit a spouse-add request (routes to approval workflow)member (group primary)SpouseAddRequestApprovalWorkflowItem
POST/api/v1/family-groups/:groupId/childrenAdd a child sub-account (auto-approved)member (group primary or spouse)ChildAddRequestUserProfile
DELETE/api/v1/family-groups/:groupId/members/:userIdRemove a member from the groupadmin204 No Content

FamilyGroup (illustrative):

{
  id:              string (UUID),
  name:            string,
  primaryMemberId: string (UUID),
  createdAt:       string (ISO 8601)
}

FamilyGroupDetail — extends FamilyGroup with:

  members: Array<{
    userId:       string (UUID),
    displayName:  string,
    relationship: "primary" | "spouse" | "child",
    role:         RoleSlug
  }>

SpouseAddRequest (illustrative):

{
  email:       string,           // spouse's email address
  firstName:   string,
  lastName:    string,
  phone:       string | null,   // optional
  displayName: string | null,   // optional; derived from firstName + lastName if omitted
}

ChildAddRequest (illustrative):

{
  firstName:   string,
  lastName:    string,
  username:    string,           // unique within platform; auto-suggested as first.last
  password:    string,           // hashed server-side; plaintext in transit over TLS only
  displayName: string | null,   // optional; derived from firstName + lastName if omitted
}

Notes:

  • POST /family-groups/:groupId/spouse creates a PENDING_APPROVAL User record, opens a SPOUSE_ADD ApprovalWorkflowItem (status pending), and sends a Clerk invitation email to the spouse's address. When the spouse signs in via Clerk, the auth layer links their Clerk subject to the pre-created record by email match (no duplicate created). The user is not added to the family group until a minister approves (see Section 6).
  • POST /family-groups/:groupId/children returns the new child UserProfile immediately (no approval step required per ADR 0007).

6. Approval Workflow &ZeroWidthSpace;

All gated events — new member join, spouse add, content publish — are routed through the ApprovalWorkflow table. Ministers and admins manage the queue through these endpoints.

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/approvalsSubmit an approval request (new member join)Public (Clerk JWT — new signup)MemberJoinRequestApprovalWorkflowItem
GET/api/v1/approvalsList all pending approval itemsministry_leader`?status=pendingapproved
GET/api/v1/approvals/:itemIdGet a single approval item detailministry_leaderApprovalWorkflowItem
POST/api/v1/approvals/:itemId/approveApprove the requestministry_leader{ note?: string }ApprovalWorkflowItem
POST/api/v1/approvals/:itemId/denyDeny the requestministry_leader{ reason: string }ApprovalWorkflowItem

ApprovalWorkflowItem (illustrative):

{
  id:            string (UUID),
  workflowType:  "member-join" | "spouse-add" | "child-add" | "content-publish",
  status:        "pending" | "approved" | "rejected",
  requestedBy:   string (UUID),   // Users.id of requester
  assignedTo:    string (UUID) | null,   // minister assigned to review
  subjectId:     string (UUID),   // e.g. the pending user ID or content ID
  note:          string | null,
  createdAt:     string (ISO 8601),
  resolvedAt:    string (ISO 8601) | null
}

MemberJoinRequest (illustrative):

{
  clerkToken:   string,   // Clerk JWT for the applicant
  displayName:  string,
  email:        string,
  note:         string | null   // optional message to the minister
}

Notes:

  • On approve for a member-join item, the API updates Users.status = active and Users.role = member, creates a new FamilyGroup for the member, and triggers a welcome notification (ADR 0013).
  • On approve for a spouse-add item, the API adds the user to the existing family group with relationship = spouse.

7. Sermons & Music Hub &ZeroWidthSpace;

The Sermons & Music Hub stores media files in Azure Blob Storage and exposes catalog metadata via SQL. Clients never receive a raw blob URI; playback and download are gated through short-lived SAS URLs issued by the API (ADR 0010).

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/mediaList/browse catalog with filtersmember`?type=videoaudio&series=<str>&speaker=<str>&from=<date>&to=<date>&limit&cursor`
GET/api/v1/media/:itemIdGet full metadata for a catalog itemmemberSermonMedia

7.2 Staff upload &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/mediaUpload a media file and create catalog entryministry_leadermultipart/form-data — file + SermonMediaCreateRequest fieldsSermonMedia
PUT/api/v1/media/:itemIdUpdate catalog metadata (no re-upload)ministry_leaderSermonMediaUpdateRequestSermonMedia
DELETE/api/v1/media/:itemIdRemove a catalog entry and its blobadmin204 No Content

7.3 Streaming and download &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/media/:itemId/stream-tokenRequest a short-lived SAS URL for web/mobile streamingmemberStreamTokenResponse
POST/api/v1/media/:itemId/download-tokenRequest a short-lived SAS URL for mobile offline downloadmemberDownloadTokenResponse

SermonMedia (illustrative):

{
  id:           string (UUID),
  title:        string,
  speaker:      string,
  series:       string | null,
  sermonDate:   string (ISO 8601 date),
  tags:         string[],
  durationSec:  number | null,
  mediaType:    "video" | "audio",
  createdAt:    string (ISO 8601),
  createdBy:    string (UUID)
  // blob_uri is NOT included — internal only
}

SermonMediaSummary — condensed (id, title, speaker, series, sermonDate, mediaType, durationSec).

StreamTokenResponse (illustrative):

{
  sasUrl:    string,   // time-limited; do not log or cache beyond playback session
  expiresAt: string (ISO 8601)
}

DownloadTokenResponse — same shape as StreamTokenResponse.

Notes:

  • SAS tokens are scoped to a single blob with a configurable expiry (initial default: 1–2 hours).
  • Clients must not log or store SAS URLs beyond the immediate playback or download operation.
  • Unsupported MIME types (anything other than .mp4, .mp3, .m4a) are rejected at upload with 400 validation_error.
  • Staff upload endpoint accepts multipart/form-data; the file field name is mediaFile. Maximum file size is enforced server-side (initial limit: configurable, documented in platform operations).

8. Community Calendar &ZeroWidthSpace;

All business logic — recurrence expansion, visibility filtering, RSVP writes, attendance recording — lives in apps/api. Clients receive concrete occurrence payloads; they never receive raw RRULE strings and never expand recurrences locally (ADR 0011).

8.1 Events &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/calendar/eventsList concrete occurrences for a date window, filtered to caller's visibility scopemember`?from=ISO8601&to=ISO8601&view=monthweek
GET/api/v1/calendar/events/:eventIdGet a single event's full metadatamemberEvent
POST/api/v1/calendar/eventsCreate an event (or recurring master + rule)ministry_leaderCreateEventRequestEvent
PUT/api/v1/calendar/events/:eventIdUpdate event metadata; scope `?occurrence=thisfollowingall` for recurring editsministry_leader (own events); admin (any)
DELETE/api/v1/calendar/events/:eventIdCancel/delete an event; `?occurrence=thisfollowingall`admin

8.2 RSVPs &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/calendar/events/:eventId/rsvpSubmit or update an RSVP for an event occurrencememberRsvpRequestEventRsvp
GET/api/v1/calendar/events/:eventId/rsvpsList RSVPs for an event (attendance planning)ministry_leader?occurrenceDate=ISO8601Paginated<EventRsvp>

8.3 Attendance (check-in) &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/calendar/events/:eventId/checkinSelf-check-in for an occurrencemember{ occurrenceDate: string }EventAttendance
POST/api/v1/calendar/events/:eventId/attendanceRecord attendance for another member (staff/kiosk)ministry_leaderStaffAttendanceRequestEventAttendance
GET/api/v1/calendar/events/:eventId/attendanceList all attendance records for an occurrenceministry_leader?occurrenceDate=ISO8601Paginated<EventAttendance>

Event (illustrative):

{
  id:              string (UUID),
  title:           string,
  description:     string | null,
  location:        string | null,
  startsAt:        string (ISO 8601 UTC),
  endsAt:          string (ISO 8601 UTC),
  allDay:          boolean,
  organizerUserId: string (UUID),
  ministryId:      string (UUID) | null,
  visibility:      "all_members" | "role_scoped" | "ministry_members" | "small_group_members",
  isCancelled:     boolean,
  isRecurring:     boolean,
  // rrule is NOT returned to clients — expansion is server-side only
  createdAt:       string (ISO 8601)
}

EventOccurrence — concrete expansion of a master event: Event fields plus occurrenceDate: string (the specific date of this instance in a recurring series).

CreateEventRequest (illustrative):

{
  title:        string,
  description:  string | null,
  location:     string | null,
  startsAt:     string (ISO 8601),
  endsAt:       string (ISO 8601),
  allDay:       boolean,
  ministryId:   string (UUID) | null,
  visibility:   VisibilityEnum,
  rrule:        string | null   // RFC 5545 RRULE string; null for one-off events
}

RsvpRequest (illustrative):

{
  occurrenceDate: string (ISO 8601 date),
  status:         "attending" | "not_attending" | "maybe",
  guestCount:     number   // additional attendees; server-enforced cap per event
}

EventAttendance (illustrative):

{
  id:               string (UUID),
  eventId:          string (UUID),
  occurrenceDate:   string (ISO 8601 date),
  userId:           string (UUID),
  checkedInAt:      string (ISO 8601),
  checkedInByUserId: string (UUID),
  method:           "self_checkin" | "staff_checkin" | "kiosk"
}

8.4 Calendar sync (ICS) &ZeroWidthSpace;

Members may subscribe to the community calendar from any standards-compliant personal calendar application using the iCalendar standard (RFC 5545 — Internet Calendaring and Scheduling Core Object Specification). Two primitives are provided (ADR 0011).

MethodPathPurposeAuthResponse shape
GET/api/v1/calendar/feed/:token/events.icsTokenized per-member ICS subscription feed; filtered to the member's visibility scopeOpaque subscription token in URL path (no bearer header — calendar apps do not send session tokens)text/calendar (RFC 5545)
GET/api/v1/calendar/events/:eventId/download.icsSingle-event .ics download for one occurrencemember (bearer token)text/calendar (RFC 5545)

Notes on ICS endpoints:

  • The subscription feed token is opaque, long, randomly generated, and stored hashed server-side. The API resolves the token to a member record and applies the identical five-role visibility filter used by GET /api/v1/calendar/events. Events the member is not permitted to see are excluded from the feed.
  • Tokens are issued only to approved members. A pending or rejected account has no token.
  • Tokens are revocable by the member (account settings) or by an admin (Section 11). On account deactivation all tokens for that member are purged.
  • The single-event download is a one-time snapshot and does not auto-refresh.
  • Two-way sync (CalDAV / write-back from personal calendar apps) is explicitly out of scope per ADR 0011. The feed is read-only; no write-back from Apple Calendar, Google Calendar, or Outlook is accepted.

Subscription token management (admin and self-service):

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/calendar/feed/tokenIssue or regenerate the caller's subscription tokenmember{ subscriptionUrl: string }
DELETE/api/v1/calendar/feed/tokenRevoke the caller's subscription tokenmember204 No Content
DELETE/api/v1/calendar/feed/token/:userIdRevoke any member's subscription tokenadmin204 No Content

Notes:

  • The ?from / ?to window is capped at 90 days per request to bound server-side recurrence expansion cost.
  • Recurring event edits use ?occurrence=this (this instance only) or ?occurrence=following (this and all future) or ?occurrence=all (the master and all instances). Default is all for PUT, this for DELETE — clients must pass the parameter explicitly to override.
  • Visitors cannot access calendar endpoints.

9. Announcements &ZeroWidthSpace;

Announcements are one-way broadcast content authored and approved by approver-capable roles. There are no reply endpoints. Recipients cannot respond at any layer (ADR 0012).

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/announcementsCreate a new announcement draftcomms_author (within assigned audience scope); group_leader, ministry_leader, admin (any audience)CreateAnnouncementRequestAnnouncement
GET/api/v1/announcementsList announcements visible to the caller's role/group (approved and not expired)member`?limit&cursor&priority=01
GET/api/v1/announcements/:announcementIdGet full announcement contentmemberAnnouncement
PUT/api/v1/announcements/:announcementIdUpdate draft or pending announcementcomms_author (own, within scope); group_leader (own); admin (any)UpdateAnnouncementRequestAnnouncement
POST/api/v1/announcements/:announcementId/submitSubmit draft for approvalcomms_author (own, within scope); group_leader (own)Announcement
POST/api/v1/announcements/:announcementId/approveApprove and publish (or schedule)ministry_leader{ note?: string }Announcement
POST/api/v1/announcements/:announcementId/rejectReject announcementministry_leader{ reason: string }Announcement
POST/api/v1/announcements/:announcementId/readMark the caller's receipt as readmember204 No Content
GET/api/v1/announcements/pendingList all announcements pending approvalministry_leader?limit&cursorPaginated<AnnouncementSummary>

Announcement (illustrative):

{
  id:             string (UUID),
  authorUserId:   string (UUID),
  approvedById:   string (UUID) | null,
  title:          string,
  body:           string,   // rich text; server-side sanitized before storage
  priority:       0 | 1 | 2,   // 0 = normal, 1 = important, 2 = urgent
  audienceRole:   RoleSlug | null,   // null = all members
  audienceGroupId: string (UUID) | null,
  status:         "draft" | "pending_approval" | "approved" | "rejected" | "expired",
  scheduledAt:    string (ISO 8601) | null,
  expiresAt:      string (ISO 8601) | null,
  createdAt:      string (ISO 8601),
  updatedAt:      string (ISO 8601)
}

AnnouncementSummary — condensed (id, title, priority, audienceRole, audienceGroupId, status, scheduledAt, createdAt).

CreateAnnouncementRequest (illustrative):

{
  title:          string,
  body:           string,
  priority:       0 | 1 | 2,
  audienceRole:   RoleSlug | null,
  audienceGroupId: string (UUID) | null,
  scheduledAt:    string (ISO 8601) | null,
  expiresAt:      string (ISO 8601) | null
}

Notes:

  • Approval authority is restricted to ministry_leader and admin roles. comms_author and group_leader must submit for approval; they cannot approve their own drafts. The API enforces approved_by_id ≠ author_user_id when the author holds the comms_author role (ADR 0023).
  • A comms_author may only create and edit announcements targeting their admin-assigned audience scope (community-wide, a specific ministry, or a specific group). Draft creation that exceeds the author's assigned scope is rejected with 403 forbidden. Audience scope assignments are managed through the Admin endpoints in Section 11.
  • Approval triggers fan-out through the platform notifications transport (ADR 0013). The Announcements feature does not call channel adapters directly.
  • The body field is sanitized server-side before storage to strip unsafe HTML. Raw HTML is not returned to clients without sanitization.
  • POST /read creates or updates an AnnouncementReceipts row for the caller. Read status is per-member, not shared.
  • Members see only announcements where their role matches audienceRole (or audienceRole is null) and their familyGroupId matches audienceGroupId (or audienceGroupId is null). Audience scoping is evaluated server-side before the response is built.

10. Notifications &ZeroWidthSpace;

The notifications transport is a platform shared service (ADR 0013). Features call it internally; it is not exposed as a public-facing feature API. The endpoints below cover member-facing notification management: registering push tokens, viewing the in-app inbox, and managing per-channel delivery preferences.

10.1 Push token management &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
POST/api/v1/notifications/push-tokenRegister (or update) a device push token for the callermemberPushTokenRequest204 No Content
DELETE/api/v1/notifications/push-tokenDeregister the caller's push token for this devicemember{ token: string }204 No Content

10.2 In-app notification inbox &ZeroWidthSpace;

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/notificationsList in-app notifications for the caller (unread first)member`?limit&cursor&unreadOnly=truefalse`
POST/api/v1/notifications/:notificationId/readMark a specific in-app notification as readmember204 No Content
POST/api/v1/notifications/read-allMark all in-app notifications as read for the callermember204 No Content

10.3 Channel preferences &ZeroWidthSpace;

Per-member channel preferences control which delivery channels are active for non-urgent notifications (ADR 0013). In-app delivery cannot be suppressed. SMS for priority = 2 (urgent) notifications cannot be suppressed for adult members with a phone on file.

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/me/notification-preferencesGet the caller's current channel preferencesmemberNotificationPreferences
PUT/api/v1/me/notification-preferencesUpdate the caller's channel preferencesmemberUpdateNotificationPreferencesRequestNotificationPreferences

NotificationPreferences (illustrative):

json
{
  "notifyByEmail": true,
  "notifyBySms": true,
  "notifyByPush": true,
  "suppressNonUrgentSms": false
}

UpdateNotificationPreferencesRequest — same shape; all fields optional (partial update).

Notes on channel preferences:

  • suppressNonUrgentSms: true suppresses SMS delivery for priority = 0 (normal) notifications. SMS is always sent for priority = 2 (urgent) regardless of this setting.
  • notifyByPush: false suppresses push dispatch from the server; OS-level push disables are respected automatically via Expo regardless of this setting.
  • Child (parent-managed) accounts have no channel preferences — they receive in-app notifications only through the parent-approved session.

PushTokenRequest (illustrative):

{
  token:    string,   // Expo push token or raw APNs/FCM token
  platform: "ios" | "android",
  deviceId: string   // stable device identifier for deduplication
}

InAppNotification (illustrative):

{
  id:          string (UUID),
  userId:      string (UUID),
  title:       string,
  body:        string,
  sourceType:  "announcement" | "calendar" | "approval" | "leadership_message" | "system",
  sourceId:    string (UUID) | null,   // links to the originating record, if applicable
  deliveredAt: string (ISO 8601),
  readAt:      string (ISO 8601) | null
}

Notes on push token management:

  • Push token registration is per-device. A member may register multiple tokens (one per device). The transport routes to all registered tokens for the user.
  • A stale or invalid token returned by APNs or FCM causes the push adapter to remove that token automatically; no client action is required.
  • Deregistering a push token on sign-out prevents stale notifications to unlinked devices.
  • The internal POST /notifications/send transport endpoint (called by features, not clients) is not documented here — it is a server-to-server call within apps/api.
  • SMS delivery is handled by the platform transport (Twilio, per ADR 0013). There are no member-facing SMS management endpoints; members control SMS delivery through channel preferences (Section 10.3).

11. Admin &ZeroWidthSpace;

Admin endpoints are gated to admin or ministry_leader roles, enforced server-side on every request (ADR 0021). The client may hide navigation for lower-privileged users as a UX convenience; the API re-checks the role regardless. Role assignments themselves are admin-only.

All six capability areas of the Admin & Ministry Portal (ADR 0021) are served by existing platform endpoints (approval queue — Section 6; member management — Section 4; family group oversight — Section 5) plus the dedicated admin endpoints below.

11.1 Audit log &ZeroWidthSpace;

The AuditLog table (ADR 0005, Layer 1) captures login/logout events, session durations, approval actions, role changes, and content publish events. This view is read-only for portal users; the API writes audit rows, never client code.

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/admin/audit-logList audit log entries with optional filtersadmin?userId=<uuid>&entityType=<str>&action=<str>&from=ISO8601&to=ISO8601&limit&cursorPaginated<AuditLogEntry>
GET/api/v1/admin/audit-log/:logIdGet a single audit log entryadminAuditLogEntry

AuditLogEntry (illustrative):

json
{
  "id":         "string (UUID)",
  "actorUserId": "string (UUID) | null",
  "action":     "string",
  "entityType": "string | null",
  "entityId":   "string (UUID) | null",
  "detail":     "object | null",
  "ipAddress":  "string | null",
  "timestamp":  "string (ISO 8601)"
}

11.2 Communications author scope assignment &ZeroWidthSpace;

A comms_author user may only draft announcements for audiences explicitly assigned by an admin. Assignments are stored in the UserCommunicationsScope table (ADR 0023).

MethodPathPurposeMin roleRequest shapeResponse shape
GET/api/v1/admin/comms-scopes/:userIdList all audience scope assignments for a comms_author useradminCommsScopeAssignment[]
POST/api/v1/admin/comms-scopes/:userIdAdd an audience scope assignmentadminCommsScopeAssignRequestCommsScopeAssignment
DELETE/api/v1/admin/comms-scopes/:userId/:scopeIdRemove an audience scope assignmentadmin204 No Content

CommsScopeAssignment (illustrative):

json
{
  "id":        "string (UUID)",
  "userId":    "string (UUID)",
  "scope":     "community | ministry | group",
  "targetId":  "string (UUID) | null"
}

targetId is null for scope = community; a ministry or group UUID otherwise.

CommsScopeAssignRequest (illustrative):

json
{
  "scope":    "community | ministry | group",
  "targetId": "string (UUID) | null"
}

Notes:

  • The API rejects a POST /announcements request from a comms_author whose draft targets an audience not covered by any of their CommsScopeAssignment rows (returns 403 forbidden).
  • Removing a scope assignment does not affect existing drafts already in pending_approval; it prevents new drafts for that audience from that author.
  • Scope assignments are logged in the AuditLog (action: comms_scope.assigned / comms_scope.revoked).

11.3 Member directory administration &ZeroWidthSpace;

Admins can correct profile data and toggle directory visibility on behalf of members. Ordinary member profile updates use PUT /api/v1/me/profile (Section 4); the endpoints below cover admin-level overrides.

MethodPathPurposeMin roleRequest shapeResponse shape
PATCH/api/v1/admin/members/:userId/directory-visibilityToggle whether a member appears in the directoryadmin{ visible: boolean }UserProfile

Notes:

  • All other admin-level member modifications (role, status, profile data correction) use the existing PUT /api/v1/members/:userId endpoint (Section 4, min role admin).

12. Shared Types Reference &ZeroWidthSpace;

The following packages/shared-types identifiers are referenced above. Each is defined once and imported by all callers. The shapes documented here are illustrative; the TypeScript definitions in packages/shared-types are the authoritative source.

TypeDomainNotes
UserMembersFull user record; includes nullable fields for child accounts
UserProfileMembersCaller-facing profile view
MemberSummaryMembersDirectory listing condensed view
FamilyGroupFamily groupsGroup header record
FamilyGroupDetailFamily groupsIncludes member list with relationships
ApprovalWorkflowItemApprovalSingle gated event (join, spouse, child-add, content)
SessionResponseAuthToken exchange result
MeResponseAuthCurrent caller identity + role
SermonMediaSermons & MusicFull catalog record
SermonMediaSummarySermons & MusicCondensed for list views
StreamTokenResponseSermons & MusicShort-lived SAS URL
DownloadTokenResponseSermons & MusicShort-lived SAS URL for offline download
EventCalendarMaster event or single occurrence
EventOccurrenceCalendarExpanded recurrence instance
EventRsvpCalendarRSVP record
EventAttendanceCalendarAttendance/check-in record
AnnouncementAnnouncementsFull announcement with workflow state
AnnouncementSummaryAnnouncementsCondensed for list views
InAppNotificationNotificationsIn-app notification record
NotificationPreferencesNotificationsPer-member channel preference settings
AuditLogEntryAdminSingle audit log record
CommsScopeAssignmentAdminPer-author audience scope assignment
RoleSlugRBACSix canonical slugs: admin, ministry_leader, group_leader, comms_author, member, visitor
VisibilityEnumCalendar`"all_members"

13. Out of scope — accepted ADRs awaiting implementation &ZeroWidthSpace;

The following feature ADRs are all Accepted as of 2026-06-18 but are scheduled for later delivery phases. API contracts for these domains will be added here when their implementation sprints begin.

  • Homeschool / Education Portal (ADR 0015)
  • Community Marketplace (ADR 0016)
  • Small Groups & Ministries (ADR 0017)
  • Pony Express Delivery Network (ADR 0018)
  • Community Ride Share & Travel (ADR 0019)
  • Sister Community Integration (ADR 0020)
  • Family Portal (ADR 0022) — member-facing family management; admin oversight covered in Section 11

Last updated: 2026-06-18

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