Appearance
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:
| Item | Summary |
|---|---|
| S1 | Data protection and COPPA |
| S2 | Trust and physical safety |
| S3 | Content 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)
| Field | Where stored | Notes |
|---|---|---|
| Full name | Users table | Required for all adults |
| Email address | Users table | Required for adults; nullable for children |
Apple / Google provider sub claim | Users table | Pseudonymous; no personal info in the claim itself |
| Profile photo | Azure Blob Storage | Optional; adults only |
| Phone number | Users table | Optional; used for SMS notifications |
| Address | Users table | Optional; used for Pony Express / Ride Share coordination |
| Family group membership | FamilyGroups + FamilyGroupMembers | Links adults to their family unit |
| Approval and role history | ApprovalWorkflow, AuditLog | Security and compliance trail |
Child sub-account data (elevated sensitivity — COPPA in scope)
| Field | Where stored | Notes |
|---|---|---|
| Username (display name or alias) | Users table | No real name required; parent chooses |
| PIN / password hash | Users table | Parent-set; hashed with Argon2id (ADR 0003), verified server-side |
| Parent FK | Users.parent_user_id | Links child to parent for cascade and consent tracking |
| Homeschool records | Homeschool feature tables | Grades, 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/apion 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(nocleartextTrafficPermitted) and the default iOS App Transport Security policy.
1.3 Data retention and deletion ​
Retention targets:
| Data category | Retention | Basis |
|---|---|---|
| Active member profiles | Indefinite (while member is active) | Operational need |
AuditLog records | 2 years rolling | Security compliance; minister oversight |
ApprovalWorkflow records | Indefinite while account is active; delete on account closure | Accountability trail |
| Sermons, announcements, media | Indefinite while content is active | Community record |
| Soft-deleted / suspended accounts | 90 days, then permanent delete | Data minimization |
| Child accounts after parent removal | 30 days post-parent deactivation, then cascade delete | COPPA data minimization |
Deletion flow (account closure):
- Admin or member requests account closure via the API.
- The API sets
Users.status = 'closed'andUsers.closed_at = now(). - A scheduled job (Azure Container Apps job or a Container Apps timer trigger — ADR 0024) runs nightly and permanently deletes records where
closed_atis older than the retention window. (Note: Azure Functions timer triggers were the ADR 0004 original choice, superseded by containerised compute in ADR 0024.) - 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).
- Profile photos in Blob Storage are deleted when
Usersrow is permanently deleted (the API issues the Blob delete as part of the deletion job; Blob Storage does not cascade automatically). - Announcements authored by a deleted user are anonymized (author FK set to null), not deleted, to preserve the community record.
- All deletion events are written to
AuditLogbefore 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.
1.4 Verifiable parental consent for child sub-accounts ​
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:
- Every adult member has been personally verified and approved by the minister before gaining platform access. The minister knows the member in person.
- 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.
- 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.emailisNULLfor allcredential_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."
- [ ]
AuditLogrecords 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:
| Event | AuditLog.event value |
|---|---|
| Child account created | child_account_created |
| Parent consent acknowledged | child_consent_recorded |
| Child PIN changed | child_credential_changed |
| Child account section restricted | child_access_restricted |
| Child account deletion requested | child_deletion_requested |
| Child account permanently deleted | child_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:
- Known to the minister by name and (typically) in person.
- Approved by the minister before gaining any platform access.
- 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 aReportstable, 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
UserBlockstable (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):
Reportstable: write access = any authenticated member; read access =adminrole only.UserBlockstable: 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:
Closed membership. Every content author is a vetted, minister-approved member. Strangers cannot post.
No user-to-user replies. Messaging is one-way broadcast only (ADR 0012,
Announcementstable with no reply FK; ADR 0007). There are no comment threads, no reaction escalations, no viral amplification paths.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.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 publishedThis 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):
| Action | Minimum role |
|---|---|
| Draft an announcement | ministry_leader |
| Approve and publish | admin |
| View published announcements | member (all members) or scoped by audience field |
| View pending / rejected drafts | admin 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:
| Invariant | Where enforced |
|---|---|
| No anonymous or unvetted accounts can post content | Closed membership + minister approval (RBAC visitor role cannot post) |
| No user-to-user reply threads | Announcements table has no reply FK; API has no endpoint to create replies |
All published content passes through ApprovalWorkflow | API rejects any direct publish to Announcements.published_at without an approved ApprovalWorkflow row |
| No user-generated public content feature is live | Member-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
Reportstable with amediacontext). - 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:
| Column | Type | Purpose |
|---|---|---|
id | UUID | Primary key |
event | varchar | Machine-readable event name (e.g. child_account_created) |
actor_user_id | FK → Users | Who performed the action |
target_user_id | FK → Users, nullable | Subject of the action (if applicable) |
target_resource_type | varchar, nullable | Resource type (e.g. announcement, child_account) |
target_resource_id | UUID, nullable | Resource ID |
metadata | JSON | Event-specific data (feature name, consent version, etc.) |
ip_address | varchar, nullable | Client IP at time of event |
created_at | timestamp | UTC 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.rolefrom 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_idand 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 ​
| Item | Status before this doc | Status after |
|---|---|---|
| S1 — Data classification | Gap | Closed — fields classified, child data enumerated |
| S1 — Encryption at rest and in transit | Gap | Closed — TDE + SSE (default-on) + TLS enforcement specified |
| S1 — Retention and deletion | Gap | Closed — retention targets and deletion flow designed |
| S1 — COPPA consent flow | Gap | Closed — step-by-step flow, consent record, minimization checklist |
| S2 — Identity assurance basis | Gap | Closed — closed membership as primary control documented |
| S2 — Coordination-only framing | Gap | Closed — no-payment, no-liability scope stated |
| S2 — Report/block capability | Gap | Closed — Reports and UserBlocks tables and flows designed |
| S2 — Marketplace public scope trigger | Gap | Closed — boundary documented; Marketplace ADR must enforce it |
| S3 — Why no separate moderation system | Gap | Closed — structural reasons documented with invariants |
| S3 — Approval workflow as moderation control | Gap | Closed — RBAC enforcement points specified |
| S3 — Re-open trigger for Phase 7 media | Gap | Closed — 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.