Appearance
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 ​
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 ​
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 ​
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 ​
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 ​
| Role | Slug | Minimum for write operations |
|---|---|---|
| Community Leader / Admin | admin | Yes |
| Ministry Leader | ministry_leader | Yes |
| Small Group Leader | group_leader | Yes |
| Communications Author | comms_author | Announcements only — draft and submit, cannot approve |
| Member | member | Read-only for most resources |
| Visitor / Prospective | visitor | Minimal — 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 ​
Handles Clerk token exchange, current-user hydration, and sign-out. All other domains assume a successfully established platform session.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/auth/session | Exchange Clerk JWT for a platform session; creates a Users row on first sign-in with role=visitor, status=pending_approval | Public (Clerk JWT required) | { clerkToken: string } | SessionResponse |
GET | /api/v1/me | Return the authenticated caller's full profile and role | visitor | — | MeResponse |
DELETE | /api/v1/auth/session | Invalidate the current platform session (sign-out) | visitor | — | 204 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
Usersrow 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).
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/auth/child-session | Authenticate a child sub-account using parent-managed credentials | Public (platform username + PIN/password) | { username: string, password: string } | SessionResponse |
4. Members & Profiles ​
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.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/me/profile | Get the caller's own profile | visitor | — | UserProfile |
PUT | /api/v1/me/profile | Update the caller's own profile (display name, photo, contact prefs) | member | UpdateProfileRequest | UserProfile |
GET | /api/v1/members | List approved members (directory) | member | ?limit&cursor&q=<search> | Paginated<MemberSummary> |
GET | /api/v1/members/:userId | Get a specific member's profile | member | — | UserProfile |
PUT | /api/v1/members/:userId | Admin update of any member's profile or role | admin | AdminUpdateUserRequest | UserProfile |
DELETE | /api/v1/members/:userId | Deactivate a member (cascades to child sub-accounts) | admin | — | 204 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
roleorstatus— those are admin-only fields. - Visitors see only their own profile via
/me/profile; they cannot access/members.
5. Family Groups ​
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).
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/family-groups | Create a new family group (triggered as part of new-member approval) | admin | CreateFamilyGroupRequest | FamilyGroup |
GET | /api/v1/family-groups | List all family groups | ministry_leader | ?limit&cursor | Paginated<FamilyGroupSummary> |
GET | /api/v1/family-groups/:groupId | Get a family group and its members | member (own group only; ministry_leader+ for any group) | — | FamilyGroupDetail |
PUT | /api/v1/family-groups/:groupId | Update group name or primary member | admin | UpdateFamilyGroupRequest | FamilyGroup |
POST | /api/v1/family-groups/:groupId/spouse | Submit a spouse-add request (routes to approval workflow) | member (group primary) | SpouseAddRequest | ApprovalWorkflowItem |
POST | /api/v1/family-groups/:groupId/children | Add a child sub-account (auto-approved) | member (group primary or spouse) | ChildAddRequest | UserProfile |
DELETE | /api/v1/family-groups/:groupId/members/:userId | Remove a member from the group | admin | — | 204 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/spousecreates a PENDING_APPROVAL User record, opens aSPOUSE_ADDApprovalWorkflowItem (statuspending), 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/childrenreturns the new childUserProfileimmediately (no approval step required per ADR 0007).
6. Approval Workflow ​
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.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/approvals | Submit an approval request (new member join) | Public (Clerk JWT — new signup) | MemberJoinRequest | ApprovalWorkflowItem |
GET | /api/v1/approvals | List all pending approval items | ministry_leader | `?status=pending | approved |
GET | /api/v1/approvals/:itemId | Get a single approval item detail | ministry_leader | — | ApprovalWorkflowItem |
POST | /api/v1/approvals/:itemId/approve | Approve the request | ministry_leader | { note?: string } | ApprovalWorkflowItem |
POST | /api/v1/approvals/:itemId/deny | Deny the request | ministry_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
approvefor amember-joinitem, the API updatesUsers.status = activeandUsers.role = member, creates a newFamilyGroupfor the member, and triggers a welcome notification (ADR 0013). - On
approvefor aspouse-additem, the API adds the user to the existing family group withrelationship = spouse.
7. Sermons & Music Hub ​
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).
7.1 Catalog — browse and search ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/media | List/browse catalog with filters | member | `?type=video | audio&series=<str>&speaker=<str>&from=<date>&to=<date>&limit&cursor` |
GET | /api/v1/media/:itemId | Get full metadata for a catalog item | member | — | SermonMedia |
7.2 Staff upload ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/media | Upload a media file and create catalog entry | ministry_leader | multipart/form-data — file + SermonMediaCreateRequest fields | SermonMedia |
PUT | /api/v1/media/:itemId | Update catalog metadata (no re-upload) | ministry_leader | SermonMediaUpdateRequest | SermonMedia |
DELETE | /api/v1/media/:itemId | Remove a catalog entry and its blob | admin | — | 204 No Content |
7.3 Streaming and download ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/media/:itemId/stream-token | Request a short-lived SAS URL for web/mobile streaming | member | — | StreamTokenResponse |
POST | /api/v1/media/:itemId/download-token | Request a short-lived SAS URL for mobile offline download | member | — | DownloadTokenResponse |
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 with400 validation_error. - Staff upload endpoint accepts
multipart/form-data; the file field name ismediaFile. Maximum file size is enforced server-side (initial limit: configurable, documented in platform operations).
8. Community Calendar ​
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 ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/calendar/events | List concrete occurrences for a date window, filtered to caller's visibility scope | member | `?from=ISO8601&to=ISO8601&view=month | week |
GET | /api/v1/calendar/events/:eventId | Get a single event's full metadata | member | — | Event |
POST | /api/v1/calendar/events | Create an event (or recurring master + rule) | ministry_leader | CreateEventRequest | Event |
PUT | /api/v1/calendar/events/:eventId | Update event metadata; scope `?occurrence=this | following | all` for recurring edits | ministry_leader (own events); admin (any) |
DELETE | /api/v1/calendar/events/:eventId | Cancel/delete an event; `?occurrence=this | following | all` | admin |
8.2 RSVPs ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/calendar/events/:eventId/rsvp | Submit or update an RSVP for an event occurrence | member | RsvpRequest | EventRsvp |
GET | /api/v1/calendar/events/:eventId/rsvps | List RSVPs for an event (attendance planning) | ministry_leader | ?occurrenceDate=ISO8601 | Paginated<EventRsvp> |
8.3 Attendance (check-in) ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/calendar/events/:eventId/checkin | Self-check-in for an occurrence | member | { occurrenceDate: string } | EventAttendance |
POST | /api/v1/calendar/events/:eventId/attendance | Record attendance for another member (staff/kiosk) | ministry_leader | StaffAttendanceRequest | EventAttendance |
GET | /api/v1/calendar/events/:eventId/attendance | List all attendance records for an occurrence | ministry_leader | ?occurrenceDate=ISO8601 | Paginated<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) ​
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).
| Method | Path | Purpose | Auth | Response shape |
|---|---|---|---|---|
GET | /api/v1/calendar/feed/:token/events.ics | Tokenized per-member ICS subscription feed; filtered to the member's visibility scope | Opaque 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.ics | Single-event .ics download for one occurrence | member (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):
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/calendar/feed/token | Issue or regenerate the caller's subscription token | member | — | { subscriptionUrl: string } |
DELETE | /api/v1/calendar/feed/token | Revoke the caller's subscription token | member | — | 204 No Content |
DELETE | /api/v1/calendar/feed/token/:userId | Revoke any member's subscription token | admin | — | 204 No Content |
Notes:
- The
?from/?towindow 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 isallfor PUT,thisfor DELETE — clients must pass the parameter explicitly to override. - Visitors cannot access calendar endpoints.
9. Announcements ​
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).
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/announcements | Create a new announcement draft | comms_author (within assigned audience scope); group_leader, ministry_leader, admin (any audience) | CreateAnnouncementRequest | Announcement |
GET | /api/v1/announcements | List announcements visible to the caller's role/group (approved and not expired) | member | `?limit&cursor&priority=0 | 1 |
GET | /api/v1/announcements/:announcementId | Get full announcement content | member | — | Announcement |
PUT | /api/v1/announcements/:announcementId | Update draft or pending announcement | comms_author (own, within scope); group_leader (own); admin (any) | UpdateAnnouncementRequest | Announcement |
POST | /api/v1/announcements/:announcementId/submit | Submit draft for approval | comms_author (own, within scope); group_leader (own) | — | Announcement |
POST | /api/v1/announcements/:announcementId/approve | Approve and publish (or schedule) | ministry_leader | { note?: string } | Announcement |
POST | /api/v1/announcements/:announcementId/reject | Reject announcement | ministry_leader | { reason: string } | Announcement |
POST | /api/v1/announcements/:announcementId/read | Mark the caller's receipt as read | member | — | 204 No Content |
GET | /api/v1/announcements/pending | List all announcements pending approval | ministry_leader | ?limit&cursor | Paginated<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_leaderandadminroles.comms_authorandgroup_leadermust submit for approval; they cannot approve their own drafts. The API enforcesapproved_by_id ≠ author_user_idwhen the author holds thecomms_authorrole (ADR 0023). - A
comms_authormay 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 with403 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
bodyfield is sanitized server-side before storage to strip unsafe HTML. Raw HTML is not returned to clients without sanitization. POST /readcreates or updates anAnnouncementReceiptsrow for the caller. Read status is per-member, not shared.- Members see only announcements where their
rolematchesaudienceRole(oraudienceRoleis null) and theirfamilyGroupIdmatchesaudienceGroupId(oraudienceGroupIdis null). Audience scoping is evaluated server-side before the response is built.
10. Notifications ​
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 ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
POST | /api/v1/notifications/push-token | Register (or update) a device push token for the caller | member | PushTokenRequest | 204 No Content |
DELETE | /api/v1/notifications/push-token | Deregister the caller's push token for this device | member | { token: string } | 204 No Content |
10.2 In-app notification inbox ​
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/notifications | List in-app notifications for the caller (unread first) | member | `?limit&cursor&unreadOnly=true | false` |
POST | /api/v1/notifications/:notificationId/read | Mark a specific in-app notification as read | member | — | 204 No Content |
POST | /api/v1/notifications/read-all | Mark all in-app notifications as read for the caller | member | — | 204 No Content |
10.3 Channel preferences ​
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.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/me/notification-preferences | Get the caller's current channel preferences | member | — | NotificationPreferences |
PUT | /api/v1/me/notification-preferences | Update the caller's channel preferences | member | UpdateNotificationPreferencesRequest | NotificationPreferences |
NotificationPreferences (illustrative):
json
{
"notifyByEmail": true,
"notifyBySms": true,
"notifyByPush": true,
"suppressNonUrgentSms": false
}UpdateNotificationPreferencesRequest — same shape; all fields optional (partial update).
Notes on channel preferences:
suppressNonUrgentSms: truesuppresses SMS delivery forpriority = 0(normal) notifications. SMS is always sent forpriority = 2(urgent) regardless of this setting.notifyByPush: falsesuppresses 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/sendtransport endpoint (called by features, not clients) is not documented here — it is a server-to-server call withinapps/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 ​
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 ​
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.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/admin/audit-log | List audit log entries with optional filters | admin | ?userId=<uuid>&entityType=<str>&action=<str>&from=ISO8601&to=ISO8601&limit&cursor | Paginated<AuditLogEntry> |
GET | /api/v1/admin/audit-log/:logId | Get a single audit log entry | admin | — | AuditLogEntry |
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 ​
A comms_author user may only draft announcements for audiences explicitly assigned by an admin. Assignments are stored in the UserCommunicationsScope table (ADR 0023).
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
GET | /api/v1/admin/comms-scopes/:userId | List all audience scope assignments for a comms_author user | admin | — | CommsScopeAssignment[] |
POST | /api/v1/admin/comms-scopes/:userId | Add an audience scope assignment | admin | CommsScopeAssignRequest | CommsScopeAssignment |
DELETE | /api/v1/admin/comms-scopes/:userId/:scopeId | Remove an audience scope assignment | admin | — | 204 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 /announcementsrequest from acomms_authorwhose draft targets an audience not covered by any of theirCommsScopeAssignmentrows (returns403 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 ​
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.
| Method | Path | Purpose | Min role | Request shape | Response shape |
|---|---|---|---|---|---|
PATCH | /api/v1/admin/members/:userId/directory-visibility | Toggle whether a member appears in the directory | admin | { visible: boolean } | UserProfile |
Notes:
- All other admin-level member modifications (role, status, profile data correction) use the existing
PUT /api/v1/members/:userIdendpoint (Section 4, min roleadmin).
12. Shared Types Reference ​
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.
| Type | Domain | Notes |
|---|---|---|
User | Members | Full user record; includes nullable fields for child accounts |
UserProfile | Members | Caller-facing profile view |
MemberSummary | Members | Directory listing condensed view |
FamilyGroup | Family groups | Group header record |
FamilyGroupDetail | Family groups | Includes member list with relationships |
ApprovalWorkflowItem | Approval | Single gated event (join, spouse, child-add, content) |
SessionResponse | Auth | Token exchange result |
MeResponse | Auth | Current caller identity + role |
SermonMedia | Sermons & Music | Full catalog record |
SermonMediaSummary | Sermons & Music | Condensed for list views |
StreamTokenResponse | Sermons & Music | Short-lived SAS URL |
DownloadTokenResponse | Sermons & Music | Short-lived SAS URL for offline download |
Event | Calendar | Master event or single occurrence |
EventOccurrence | Calendar | Expanded recurrence instance |
EventRsvp | Calendar | RSVP record |
EventAttendance | Calendar | Attendance/check-in record |
Announcement | Announcements | Full announcement with workflow state |
AnnouncementSummary | Announcements | Condensed for list views |
InAppNotification | Notifications | In-app notification record |
NotificationPreferences | Notifications | Per-member channel preference settings |
AuditLogEntry | Admin | Single audit log record |
CommsScopeAssignment | Admin | Per-author audience scope assignment |
RoleSlug | RBAC | Six canonical slugs: admin, ministry_leader, group_leader, comms_author, member, visitor |
VisibilityEnum | Calendar | `"all_members" |
13. Out of scope — accepted ADRs awaiting implementation ​
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