← 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
- Filing. The phone calls
report_messageorreport_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. - 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 storedunchecked. Nothing decrypts or fetches content for a report: the server holds no decrypt path, and none was built. - Stored first, then the notice (C11). The row is written by the filing call. pg_cron calls
deliver-report-noticesevery 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 existingRESEND_API_KEYandFEEDBACK_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. - Urgency (C32) is derived, not stored: the categories
threats,self_harm,sexual_contentandunderage, or the under-18 flag (the reporter's or the reported account's age band). It chooses the email subject and nothing else. - 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. - Blocking (C15, C16, C33, C34).
block_membertakes 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 achild_guardianslink in either direction. Every failure is one error,block_refused.
4. Who sees what
- The reporter sees their own reports' kind, category, note, text, date and status, never the outcome or the operator's note (C5). "Download my data" includes them (C14).
- The reported person, admins and guardians have no read path (C5, C19): no client role holds a
grant on
member_reports, and no member-facing function selectsreporter_id. A guardian sees nothing of a child's reports. - Blocks are invisible to everyone but the blocker (C18). There's no count and no "blocked by".
A recipient who blocked the actor is claimed and recorded like one who was sent the push, but
nothing is sent to them. The sender's answer is given before anything is sent and doesn't depend
on who was sent what: a targeted notice says only whether anything was claimed, and a task push
says the same thing every time. A repeat call, a retry or a failed send goes the same way with
or without a block, and the answer carries no count of sends, devices or recipients.
A blocked person's direct invitation is refused with
unavailable, the same outcome as an ineligible recipient's. - Nothing automatic follows a count (C9). Twenty reports on one member change nobody's view.
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
- HQ PIA-007,
pias/pia-007-report-block-ai-disclosure.md(Part A, conditions C1–C33), approved 27 October 2026. - Migration
supabase/migrations/20261027221815_member_reports_and_blocks.sql. supabase/functions/deliver-report-notices/(the notice),supabase/functions/_shared/blocks.ts(the send-first lookup),supabase/functions/export-my-data/index.ts(C14).- Tests:
__tests__/member_reports_and_blocks.test.ts,test-utils/dbBlocks.ts(called from the event confirmation, targeted notice, connected group, mute and direct invitation suites),supabase/functions/_shared/__tests__/blocks.test.ts,supabase/functions/deliver-report-notices/__tests__/deliver.test.ts. - Operator:
docs/runbooks/member-reports.md,docs/runbooks/operator-access-register.md.