← All privacy impact assessments

PIA: report a message or a member to GroupApp, and block a member

Status: this assessment is HQ's PIA-007 Part A (approved with conditions C1–C33 by the founder on 27 October 2026), restated as built. The server half was built on 27 October 2026 (one database change and one delivery function; see Sources). The sheets, the hiding of a blocked person's messages and the Block button are the app half (s2-report-app). Neither reaches members before the privacy policy, Terms and child policy that describe them are on phones (C22). Part B of PIA-007, the adult AI disclosure, is assessed in the cloud inference assessment. Nothing here is legal advice.


1. Purpose and scope

Why: a member who's harassed, threatened or shown something harmful by another member needs a way to tell someone who can act on it, and a way to stop seeing it. Admins can't act on a message: there's no admin delete of another member's message, an admin may be the person reported, and in a family group the admin is often the reporter's parent. Apple's guideline 1.2 expects the developer to respond. So a report goes to GroupApp (the founder, as operator), never to the group's admins (F1).

This is the second route by which member content reaches our team on purpose, after "Send feedback to GroupApp" (its assessment), and the first to carry another person's words.

In scope: reporting one message, one chat photo's caption, or one member; what's stored and for how long; the operator's notice and the operator's actions; blocking a member and what a block changes on the server. Out of scope: the photo wall's own reports, which still go to that group's admins (the photo wall assessment §5; F10); the app's sheets and hiding (the app half).

2. Data involved

Item Holds Read by Kept
member_reports kind (message or member), a category from a closed list, a note of up to 500 characters, the reported account, the group, the message id and time, photo (a chat photo's caption), the text the reporter's phone sent (message reports only, up to 4,000 characters), the content check (matched or unchecked), the reporter, both age bands (and the "under 18 involved" flag derived from them), the group's level and whether it has a group key, the app version, the status (received, reviewed), the outcome, the dates (filed, closed, legal hold and its reason) and a random 10-character reference the reporter (list_my_reports: kind, category, note, text, date, status); the founder as report_reviewer §6
member_blocks blocker, blocked, when the blocker (own-row RLS); has_blocked, asked by the push paths and direct invitations until unblocked or either account is deleted
member_safety_actions that a person filed a report or made a block, and when; no target only the limit checks 30 days
The operator notice reference, kind, category, time, "under 18 involved"; subject "GroupApp: report (ref …)" or "GroupApp: urgent report (ref …)" the founder, at help@groupapp.ai deleted by hand

Categories, as stable ids: harassment (harassment or bullying), threats, sexual_content, hate, self_harm (may hurt themselves), scam_spam, impersonation, underage (under 13 or lying about age), other.

Read, not stored: the reported group_messages or group_chat_photos row (author, group, time and, for an unsealed row only, its content, for an equality check inside the filing function).

Not collected, and could have been: no image, ever (C7); no thread or other messages; no device identifiers or location; no copy of a stored object. A chat-photo report holds only the caption the phone can read and photo: true. A chat message that carries a photo is reported by its words alone: the link to the photo is removed before the content check and is never stored.

3. Flows

  1. Filing. The phone calls report_message or report_member. In this order, so that an existing target and a missing one get the same answer (C10): the input; that the caller is a person (is_person, so never a wall or a profile); the caller's limits; then the target (C2). A message report needs the caller active in its group and a person's message, not a system row or their own, and it reports the author. A member report needs someone sharing an active group with the caller, or a pending requester to a group the caller administers. Every target failure is one error, report_refused.
  2. The content check (C4). For an unsealed row the text the phone sent must equal the stored row; a mismatch is report_refused, so the caller learns nothing about what's stored. Messages have no UPDATE, so a mismatch is tampering. A sealed row is stored unchecked. Nothing decrypts or fetches content for a report: the server holds no decrypt path, and none was built.
  3. Stored first, then the notice (C11). The row is written by the filing call. pg_cron calls deliver-report-notices every five minutes through pg_net with a Vault token. It claims each notice just before its send, so a withdrawn report is never emailed, and sends it to help@groupapp.ai through Resend with the existing RESEND_API_KEY and FEEDBACK_EMAIL_FROM. A failed send is retried up to ten times; the report is never lost. The function holds no grant on the table, only the four delivery functions, and its logs hold counts.
  4. Urgency (C32) is derived, not stored: the categories threats, self_harm, sexual_content and underage, or the under-18 flag (the reporter's or the reported account's age band). It chooses the email subject and nothing else.
  5. Review (C6, C12, C20). The founder reads reports through report_reviewer (nologin noinherit, column grants, switched to login only for a session) and acts through definer functions: remove_reported_item (the message or chat photo row, with no tombstone), close_member_report (no_action, removed, warned, suspended, referred) and the legal hold. Suspension is in the Supabase Auth console. Each session is logged in the operator access register. Runbook.
  6. Blocking (C15, C16, C33, C34). block_member takes effect at once and never reads a report. The target is someone the app can show the caller: a person in an active group they share now; a pending requester to a group the caller runs; or the author of a message or chat photo still held in a group the caller is active in. Anyone else, someone who once shared a group included, is refused. Never self, a wall or a profile, or across a child_guardians link in either direction. Every failure is one error, block_refused.

4. Who sees what

5. What a block does on the server (C16)

A block changes only what reaches the blocker. Pushes caused by the blocked person's act are skipped:

Push Where the skip is Actor
Chat message send-message-notification (blockerLookup → withoutBlockers) the sender
Daily catch-up catchup_push_candidates leaves the blocked person's messages out of the count the senders
Task assignment and join request send-action-notification the caller
Event confirmation claim_event_confirm_deliveries, claim_event_confirm_delivery the requester
Task notice claim_task_notice_deliveries whoever added that recipient
Connection request claim_connection_request_deliveries the requesting admin
Photo report (to admins) claim_photo_report_deliveries the reporter
Key access request (to key holders) claim_group_key_access_deliveries; the request stays open and every owner and admin still sees it in the app, so the blocked person can still be given the key the requester
Baby Live Activity update dispatch-glance-updates whoever logged the feed
Chat unread counts (the badge) group_chat_unread_count, which the app's count reads too the senders
Direct invitation create_direct_invitation refuses; list_received_direct_invitations, claim_direct_invitation_deliveries and direct_invitation_delivery_is_eligible skip one made before the block the sender

Because the answer comes first, a sender is no longer told when a push failed or offered a retry: a refused send is retried for a few seconds in the background and then recorded as failed. The notice itself is recorded and visible either way. A failed block lookup in the three send-first functions sends nobody that push (a block is a safety choice; a mute fails open because it's a preference).

Not skipped, on purpose: task and event reminders (they fire on a shared record's own time, whoever set them; tasks and events stay as they are, F6), Mail Sifter's suggestions (no member acts), and pushes the blocker causes. The baby records behind a skipped Live Activity update are unchanged: the blocker's phone shows the real feed when the app opens, and their Lock Screen countdown can be stale until then (a residual, §10). The Realtime chat fan-out is unchanged: hiding messages on the blocker's phones is the app half (C17).

6. Retention and deletion (C13)

A nightly job (sweep_member_reports, 03:50 UTC) deletes the text, note and operator note 30 days after close, and at most 180 days after filing; an open report then closes expired (keeping status received, because nobody reviewed it). It deletes the outcome record 12 months after close. Only the operator sets a legal hold, for a recorded reason, and a hold stops all three. A deleted reporter's id goes null. A deleted reported account or group takes the text and note at once, by a trigger on the foreign keys' set null, unless a legal hold is set. A reporter can withdraw an open report, which deletes it. The rate-limit log goes after 30 days. When the operator clears a hold on a report whose account or group was deleted meanwhile, its text, note and operator note go at that moment.

7. Children (C19)

13 to 17 year olds report and block like adults, except across a child_guardians link. The guardian isn't told and has no read path. A report involving anyone under 18 is urgent (C32) and looked at within 24 hours. A reported child's message may be removed; anything further goes through their guardian. Kids Helpline, eSafety and 000 on their sheet are the app half.

8. Online Safety Rules s 4A(5)(a)

No member is shown how others engaged with what they posted: the reported person is never told, and no count exists. The reporter sees only the operator's status on their own report, and the operator isn't an end-user. Not a feedback feature (C18); the ban self-assessment records it.

9. Structural versus conventional

Property How it holds Pinned by
No client writes; no client reads reports grants; RLS on all three tables the reports-and-blocks database tests (C1)
One refusal for every target failure; limits before targets function order the same tests (C2, C10)
No decrypt or fetch path the function compares only an unsealed row's stored text the same tests (C4, C7)
Nothing else reads reports or blocks catalogue and source scans the same tests (C18) and the push-path source scans
Every push path skips a blocker has_blocked in each claim; withoutBlockers in each sender a block contract beside each claim's mute contract; the push-path unit tests
The notice carries no content claim_report_notice returns only the five fields the notice function's unit tests
Not suspending on one unchecked report (R3), the response targets, the suspension email conventional: the runbook —

10. Residual risk we accept (PIA-007 §7)

R1 a reported message from an encrypted or Device-only group sits in plaintext for up to §6's period, on the reporter's choice alone; R2 in a small group the reported person can guess who reported; R3 a sealed message can be misquoted; R4 one operator, so the 24-hour aim slips when they're away; R5 a block is partial, and a blocker's baby Live Activity can be stale until the app opens; R5a removing an encrypted message that showed a photo can't remove the photo, because the server can't tell which one it was; R6 a suspended person can sign up again; R7 no pre-posting filter; R8 a week, one reviewer, revisited before the public App Store listing.

Amended 31 October 2026 (HQ PIA-016, reactions and deleting your own message). A member can now delete a message they sent, and a linked parent or guardian can delete their own child's message (the child is under 18). There is still no admin delete: an owner or admin cannot delete anyone else's message, and the operator's removal of a reported item is unchanged. Deleting never cancels a report: a report filed before the delete keeps the reporter's copy of the message and is swept on its own schedule. A deleted message can't be reported as a message, because nothing of it is left to check or keep; report_message refuses it with the same answer as any other target it can't take, and the member who sent it can still be reported or blocked (PIA-016 R2).

11. Review trigger

Reassess before any of these, each of which PIA-007 marks Blocked: an image in a report; server decryption or fetching for a report; a model reading reports or blocks; automatic action on report counts; the reporter revealed. Also before a second reviewer, or any change to the retention periods (which the policy and the sheet state).

12. Sources