Skip to content

Security HIGH Items — Design ​

Audience: Platform team (internal only)

Status: Design complete — these items must be implemented before the platform (Phase 1 / AB#3074) ships. The plan (pmo/platform-strategy.md) marks the platform as "not build-ready until the HIGH items below are designed." This document closes that gap.

ADO tracking: Security design work is part of the Platform Epic (AB#3074). Each section below maps to a user story under that epic.


Scope ​

This document designs three HIGH-severity security items identified in the CAF / WAF audit:

ItemSummary
S1Data protection and COPPA
S2Trust and physical safety
S3Content moderation

Medium and low items (S4–S8) are tracked in the platform strategy and do not block build-start.


S1 — Data protection and COPPA ​

1.1 Data classification ​

The platform handles two categories of personal data:

Adult member PII (standard sensitivity)

FieldWhere storedNotes
Full nameUsers tableRequired for all adults
Email addressUsers tableRequired for adults; nullable for children
Apple / Google provider sub claimUsers tablePseudonymous; no personal info in the claim itself
Profile photoAzure Blob StorageOptional; adults only
Phone numberUsers tableOptional; used for SMS notifications
AddressUsers tableOptional; used for Pony Express / Ride Share coordination
Family group membershipFamilyGroups + FamilyGroupMembersLinks adults to their family unit
Approval and role historyApprovalWorkflow, AuditLogSecurity and compliance trail

Child sub-account data (elevated sensitivity — COPPA in scope)

FieldWhere storedNotes
Username (display name or alias)Users tableNo real name required; parent chooses
PIN / password hashUsers tableParent-set; hashed with Argon2id (ADR 0003), verified server-side
Parent FKUsers.parent_user_idLinks child to parent for cascade and consent tracking
Homeschool recordsHomeschool feature tablesGrades, curriculum — Phase 7; COPPA in scope

What the platform does not collect for children:

  • No email address.
  • No photo.
  • No phone number or address.
  • No location data.
  • No behavioral tracking or advertising identifiers.
  • No data is sent to Clerk — child credentials are platform-owned only (ADR 0003).

1.2 Encryption at rest and in transit ​

At rest:

  • Azure Database for PostgreSQL Flexible Server (ADR 0024): Transparent Data Encryption (TDE) is enabled by default on Azure Database for PostgreSQL and cannot be disabled. No additional configuration is required; encryption is automatic. (Note: the ADR 0004 choice of Azure SQL Serverless was superseded by ADR 0024 in favour of PostgreSQL on Flexible Server.)
  • Azure Blob Storage: Server-side encryption (SSE) with Microsoft-managed keys is enabled by default on all Blob Storage accounts. No additional configuration is required.
  • Key Vault (kv-hcs-vault-01): All application secrets (connection strings, API keys) are stored in Key Vault. No secrets in application code, config files, or environment variables committed to the repository.

In transit:

  • All traffic between clients (web, mobile) and the API (apps/api on Azure Functions) uses TLS 1.2 minimum. Azure Functions enforces HTTPS-only by default; disable HTTP access in the Function App configuration.
  • All traffic between the API and Azure SQL uses TLS. The Azure SQL connection string must include Encrypt=True;TrustServerCertificate=False.
  • Azure Blob Storage access is HTTPS-only by default.
  • Clerk's token exchange uses TLS; the Clerk SDK enforces this.
  • Mobile clients (React Native + Expo) do not permit cleartext HTTP connections. Enforce via Android network_security_config.xml (no cleartextTrafficPermitted) and the default iOS App Transport Security policy.

1.3 Data retention and deletion ​

Retention targets:

Data categoryRetentionBasis
Active member profilesIndefinite (while member is active)Operational need
AuditLog records2 years rollingSecurity compliance; minister oversight
ApprovalWorkflow recordsIndefinite while account is active; delete on account closureAccountability trail
Sermons, announcements, mediaIndefinite while content is activeCommunity record
Soft-deleted / suspended accounts90 days, then permanent deleteData minimization
Child accounts after parent removal30 days post-parent deactivation, then cascade deleteCOPPA data minimization

Deletion flow (account closure):

  1. Admin or member requests account closure via the API.
  2. The API sets Users.status = 'closed' and Users.closed_at = now().
  3. A scheduled job (Azure Container Apps job or a Container Apps timer trigger — ADR 0024) runs nightly and permanently deletes records where closed_at is older than the retention window. (Note: Azure Functions timer triggers were the ADR 0004 original choice, superseded by containerised compute in ADR 0024.)
  4. Child sub-accounts of a closed parent are flagged for deletion on the same schedule (30-day window — shorter than adult window to minimize child data exposure).
  5. Profile photos in Blob Storage are deleted when Users row is permanently deleted (the API issues the Blob delete as part of the deletion job; Blob Storage does not cascade automatically).
  6. Announcements authored by a deleted user are anonymized (author FK set to null), not deleted, to preserve the community record.
  7. All deletion events are written to AuditLog before the row is removed.

Right to erasure: because this is a closed, minister-approved community (not a public service), the minister / admin is the data controller. A member requesting erasure goes through the minister. The deletion flow above satisfies the request mechanically.

Basis for the consent model:

COPPA (Children's Online Privacy Protection Act) requires verifiable parental consent before collecting personal information from children under 13. The platform's closed, minister-approved membership model provides a structural basis for consent that is stronger than a typical open-registration site:

  1. Every adult member has been personally verified and approved by the minister before gaining platform access. The minister knows the member in person.
  2. When a parent adds a child sub-account, the parent is already a verified, approved member. The parent's prior minister approval serves as the identity assurance layer.
  3. The child's data is minimal by design (username + PIN hash only; no email, photo, or contact information — see Section 1.1).

This is the "direct notice to parent" + "actual knowledge" model recognized by the FTC: the platform has actual knowledge that the account holder is a parent (via minister approval) and the parent provides direct consent by submitting the child creation request.

Consent flow — step by step:

Step 1 — Parent is a vetted member
  The parent's account has status = 'approved' in Users.
  The minister's approval record exists in ApprovalWorkflow
  (workflow_type = 'member-join', status = 'approved').

Step 2 — Parent initiates child account creation
  Parent opens the Family Portal (member-only, RBAC-gated to role = 'member').
  Parent fills in: child display name (alias, not required to be real name),
  child age bracket (under-13 flag, not exact birthdate), and sets a PIN.
  The form presents a consent acknowledgement:
    "I am the parent or legal guardian of this child. I consent to the
    platform creating an account for my child with the information I have
    provided. I understand this account is limited to [homeschool / approved
    sections] and does not collect my child's email address, photo, or
    contact information."
  Parent checks the box and submits.

Step 3 — Platform creates the child account
  API validates: parent's account is status = 'approved'.
  API creates Users row:
    credential_type = 'parent-managed'
    email = NULL
    parent_user_id = parent's Users.id
    under_13 = true (if flagged by parent)
    username = parent-provided alias
    password_hash = Argon2id hash of parent-set PIN (ADR 0003)
    status = 'active'
  API writes ApprovalWorkflow row:
    workflow_type = 'child-account-create'
    requested_by = parent's Users.id
    reviewed_by = NULL (auto-approved — parent is already vetted)
    status = 'auto_approved'
    consent_acknowledged_at = now()
    consent_version = current consent text version
  AuditLog entry written: event = 'child_account_created',
    actor = parent, target = new child Users.id.

Step 4 — No email or social login issued
  The child account has no Clerk session. Authentication is handled
  entirely by the platform's own credential check (username + PIN).
  The auth endpoint for parent-managed accounts is separate from the
  Clerk OAuth flow (see ADR 0003).

Step 5 — Parent-visible record
  The Family Portal shows the parent a list of their child sub-accounts
  with: display name, date created, sections accessible, last login date.
  The parent can change the child's PIN, restrict sections, or request
  account deletion at any time.
  Deletion request by parent triggers the 30-day deletion window (Step 6
  of the deletion flow in Section 1.3).

Consent record retention:

The ApprovalWorkflow row with consent_acknowledged_at and consent_version is the consent record. It is retained for the life of the child's account plus 30 days post-deletion.

Under-13 flag:

The Users.under_13 boolean column gates data collection. When under_13 = true, the API must:

  • Reject any request to add an email address, profile photo, or phone number to the account.
  • Exclude the account from any behavioral analytics.
  • Return only the child's own data in API responses — never cross-reference to other members.

Minimum data — implementation checklist:

  • [ ] Users.email is NULL for all credential_type = 'parent-managed' rows. Enforce with a DB check constraint: email IS NOT NULL OR credential_type = 'parent-managed' (from ADR 0007).
  • [ ] No photo upload endpoint accepts a request where the authenticated user has under_13 = true.
  • [ ] No notification (email/SMS) is sent to a child account. Notifications go to the parent.
  • [ ] Child session tokens are short-lived (max 4 hours); no persistent "remember me."
  • [ ] AuditLog records child login events (actor = child, supervised by parent FK).

If the homeschool portal is built (Phase 7):

Homeschool records (grades, curriculum progress) are COPPA-in-scope data. The same minimization rules apply. Homeschool data is not accessible to other members; only the child's teacher(s) and parent can read it. The homeschool ADR (ADR 0015) must cross-reference this section.

1.5 Connection to audit logging (ADR 0005) ​

Every event in the consent and child-account lifecycle writes an AuditLog row:

EventAuditLog.event value
Child account createdchild_account_created
Parent consent acknowledgedchild_consent_recorded
Child PIN changedchild_credential_changed
Child account section restrictedchild_access_restricted
Child account deletion requestedchild_deletion_requested
Child account permanently deletedchild_account_deleted

Minister / admin can view these events in the admin dashboard (filtered by event LIKE 'child_%').


S2 — Trust and physical safety ​

2.1 Identity assurance via closed membership ​

The platform's primary physical-safety control is structural: this is a closed, minister-vetted community, not an open marketplace. Every adult member who appears in in-person coordination features (Pony Express, Ride Share, events) has been:

  1. Known to the minister by name and (typically) in person.
  2. Approved by the minister before gaining any platform access.
  3. Linked to a real email address (Apple/Google identity) that was validated at sign-up by Clerk.

This is materially different from a public marketplace where a stranger's identity is unverified. The minister-approval gate is the primary safety control; it does not need to be augmented by document verification, credit checks, or third-party identity services at the current community scale (~200 members).

2.2 In-person coordination features — scope and framing ​

The platform's "Signature Features" (Pony Express, Ride Share, Community Travel) are:

  • Coordination-only. The platform shows who is offering a service or ride and allows members to express interest or RSVP. No payments are processed on the platform. No contracts are formed on the platform. Members arrange the details off-platform (phone, in person).
  • No platform liability. The platform does not broker, guarantee, or insure any service. Participation is between consenting members of an established community.
  • No public access. These features are member-only (RBAC-gated to role IN ('member', 'group_leader', 'ministry_leader', 'admin')). Visitors cannot access them.

2.3 Liability and waiver acknowledgement ​

When a member first accesses a coordination feature (Pony Express, Ride Share), the platform presents a one-time acknowledgement:

"This feature coordinates volunteer services between members of Heritage Virginia. Heritage Community Hub does not provide, guarantee, or insure any service listed here. Participation is voluntary and at your own discretion. By continuing, you acknowledge that you are arranging services directly with fellow members, not with the platform."

The acknowledgement is recorded in AuditLog (event = 'coordination_feature_acknowledged', actor = user_id, metadata = { feature: 'pony-express' | 'ride-share' }). It is shown once per feature, not on every visit.

This is a light-touch acknowledgement appropriate for a closed faith community — not a legal contract. It documents that the member understood the coordination-only framing.

2.4 Report and block capability ​

Even in a vetted community, members must have a way to flag a concern. The platform implements:

Report:

  • Any member can submit a report against any other member's listed service or profile.
  • Report is submitted to the API (POST /reports), stored in a Reports table, and routed to the admin / minister queue (visible in the admin dashboard).
  • Report fields: reporter ID, reported user ID, feature context (pony-express / ride-share / event), free-text description (max 1,000 characters), timestamp.
  • The reported user is not notified of the report (to prevent retaliation before review).
  • The minister reviews and takes action: warn, suspend, or remove the reported user's access.
  • All report actions are written to AuditLog.

Block:

  • Any member can block another member (POST /users/{id}/block).
  • A block hides the blocker's profile and listings from the blocked user and vice versa within coordination features. It does not remove the blocked user from the platform.
  • Block is stored in a UserBlocks table (blocker_id, blocked_id, created_at).
  • The API filters all coordination-feature queries through the block table for the requesting user.
  • Block is silent — the blocked user is not notified.

RBAC connection (ADR 0006):

  • Reports table: write access = any authenticated member; read access = admin role only.
  • UserBlocks table: write/read = the blocking user only (they can see their own block list); admin can read for moderation purposes.
  • Server-side enforcement only — clients never assert their own access level.

2.5 S2 scope boundary — if marketplace goes public ​

S2 is substantially mitigated by the closed membership. If the Community Marketplace feature is ever opened to the public (non-vetted accounts), the following controls become required and S2 re-opens as a HIGH item:

  • Third-party identity verification (government ID or equivalent).
  • Formal terms of service with a legally binding dispute process.
  • Payment processor liability handling (if transactions are ever introduced).
  • Content moderation at scale (see S3).

This is a build-time trigger, not a current requirement. The Marketplace ADR (ADR 0016) must note this boundary explicitly.


S3 — Content moderation ​

3.1 Why a separate moderation system is not needed ​

Content moderation on open platforms requires distinct infrastructure (automated classifiers, human review queues, appeals processes) because any user can publish to any other user, at volume, without prior approval.

Heritage Community Hub does not have this problem. The structural controls that eliminate the need for a separate moderation system are:

  1. Closed membership. Every content author is a vetted, minister-approved member. Strangers cannot post.

  2. No user-to-user replies. Messaging is one-way broadcast only (ADR 0012, Announcements table with no reply FK; ADR 0007). There are no comment threads, no reaction escalations, no viral amplification paths.

  3. Approver-authors-and-approves model. Content (announcements, calendar events) is published through the existing ApprovalWorkflow. An author with appropriate role drafts content; an approver reviews it before it is published to recipients. The approver is the moderation layer.

  4. No user-generated public content. The member-posted media feature (photos) is explicitly back-burnered. When it ships (Phase 7), it will require a moderation design. Until then, no member-generated content is published without approval.

3.2 The approval workflow as the moderation control ​

The ApprovalWorkflow table (ADR 0007) handles all content gating:

Author (role: ministry_leader or admin) drafts content
  → API creates ApprovalWorkflow row (status = 'pending', workflow_type = 'content-publish')
  → Approver (role: admin) reviews in the admin dashboard
  → Approver approves or rejects
  → On approval: content published (Announcements.published_at set, AuditLog entry written)
  → On rejection: author notified with reason; content not published

This flow means the approver has reviewed every piece of content before any member sees it. There is no path for unreviewed content to reach recipients.

RBAC connection (ADR 0006):

ActionMinimum role
Draft an announcementministry_leader
Approve and publishadmin
View published announcementsmember (all members) or scoped by audience field
View pending / rejected draftsadmin only

Server-side enforcement: the API checks Users.role from the database on every content action. The client never asserts publish or approval rights.

3.3 Controls that keep the no-moderation-system assumption valid ​

The assumption holds as long as the following design invariants are maintained. Violating any of these re-opens S3 as a HIGH item and requires a separate moderation design:

InvariantWhere enforced
No anonymous or unvetted accounts can post contentClosed membership + minister approval (RBAC visitor role cannot post)
No user-to-user reply threadsAnnouncements table has no reply FK; API has no endpoint to create replies
All published content passes through ApprovalWorkflowAPI rejects any direct publish to Announcements.published_at without an approved ApprovalWorkflow row
No user-generated public content feature is liveMember-posted media is back-burnered; when it ships, a moderation design is required first

3.4 If member-posted media ships (Phase 7) ​

When member-posted photo media is built, S3 re-opens. At minimum the design must include:

  • Pre-publication review by an admin before photos are visible to other members.
  • A report mechanism (already designed in S2.4 — reuse Reports table with a media context).
  • Retention and deletion of photos associated with closed accounts (already covered in S1.3).

The member-posted media ADR (part of Phase 7) must reference this section and close S3 explicitly for that feature before build begins.


Cross-cutting: audit logging and server-side RBAC ​

Audit logging (ADR 0005) ​

Every security-relevant event in S1, S2, and S3 writes to the AuditLog table (Layer 1 of the four-layer observability model). The table schema must include:

ColumnTypePurpose
idUUIDPrimary key
eventvarcharMachine-readable event name (e.g. child_account_created)
actor_user_idFK → UsersWho performed the action
target_user_idFK → Users, nullableSubject of the action (if applicable)
target_resource_typevarchar, nullableResource type (e.g. announcement, child_account)
target_resource_idUUID, nullableResource ID
metadataJSONEvent-specific data (feature name, consent version, etc.)
ip_addressvarchar, nullableClient IP at time of event
created_attimestampUTC timestamp

The AuditLog is write-only for all non-admin roles. Admin can read; no one can delete or update. This is enforced server-side — there is no API endpoint to update or delete AuditLog rows.

Server-side RBAC (ADR 0006) ​

All authorization decisions in S1, S2, and S3 follow the two-plane model:

  • The API reads Users.role from the database on every request — never from a client-supplied claim.
  • Children's section access is enforced as an additional application-layer check on top of role: the API checks Users.parent_user_id and the parent's approved section list before returning homeschool or family-portal data to a parent-managed session.
  • Report and block endpoints enforce that the actor and target are both status = 'active' community members before accepting the operation.
  • Content approval endpoints enforce role = 'admin' at the API layer — not the client layer.

Summary — what this document closes ​

ItemStatus before this docStatus after
S1 — Data classificationGapClosed — fields classified, child data enumerated
S1 — Encryption at rest and in transitGapClosed — TDE + SSE (default-on) + TLS enforcement specified
S1 — Retention and deletionGapClosed — retention targets and deletion flow designed
S1 — COPPA consent flowGapClosed — step-by-step flow, consent record, minimization checklist
S2 — Identity assurance basisGapClosed — closed membership as primary control documented
S2 — Coordination-only framingGapClosed — no-payment, no-liability scope stated
S2 — Report/block capabilityGapClosed — Reports and UserBlocks tables and flows designed
S2 — Marketplace public scope triggerGapClosed — boundary documented; Marketplace ADR must enforce it
S3 — Why no separate moderation systemGapClosed — structural reasons documented with invariants
S3 — Approval workflow as moderation controlGapClosed — RBAC enforcement points specified
S3 — Re-open trigger for Phase 7 mediaGapClosed — trigger documented; Phase 7 ADR must close it

The platform (Phase 1 / AB#3074) is now design-ready for the HIGH security items. Build teams should treat this document as a required input to the implementation of any feature in scope above.


Last updated: 2026-06-18.

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