← All privacy impact assessments
PIA: emoji reactions on group-chat messages, and deleting your own message
Register status: Approved with conditions (C1–C15), founder, 31 October 2026. Delete: built (this change: the tombstone, the guardian removal, the readers and the line). Reactions: built (31 October 2026: the table with three slots, the closed list, sealed emoji, the private poke, the chips and the sheet, and the legal record: the ban self-assessment's §16 and the accepted §15.9 finding). The privacy-policy text (C14) goes in a separate policy change. This page is a copy of HQ's PIA-016, including the founder's answers, with migration references written as dates. Nothing here is legal advice.
Amended 1 November 2026 (HQ PIA-016 addendum, "the organising reaction set, and reaction pushes to the author", Approved; this change is the first of its two, under conditions A1–A4, A17 and A18). Reactions became one reaction per member per message, from five organising answers: 👍 Got it (with an optional when: in 15 min, later today or tomorrow), ✅ Done, ❌ Can’t do it, 🙋 Need a hand and ❤️ Thanks. Choosing another replaces yours; choosing yours again takes it off. There is no emoji picker. C4's cap of 3 becomes 1 and C5's list of 46 becomes the 8 stored values below; see "As built: the organising set". The rest of this page is the 31 October record and is left as written. Pushes to the author came in the second change under the same addendum (A5–A18, with the founder's amendment that every answer notifies, Thanks included); see "As built: the author's notification". C3 is replaced by them.
- PIA ID:
2026-10-31-chat-reactions-and-delete. Re-opens the under-16 ban self-assessment (§§5.3, 10 item 0, 12, 15.9); amends the child accounts assessment, the privacy posture, the member safety reports and blocks assessment (PIA-007) and the notification previews assessment (PIA-010). - Feature: founder 2026-10-31 (decision log): reactions return, at most 3 emoji per member per message; a member can delete their own group-chat message, leaving a "message deleted" line.
- Reviewer: security lens. Written 2026-10-31 against app
origin/integration66cb839f(policyv3.14-2026-10-28live,v3.15-2026-11-03staged; child policyv1.7-2026-10-23). - Status: Approved with conditions (C1–C15), founder, 2026-10-31 ~03:20 Sydney ("Approve as recommended"); answers below.
The founder has chosen to bring reactions back and keep under-16 members, knowing the regulatory risk. This PIA does not re-argue that choice. It sets out what the record must now say, the strongest truthful position, and the conditions that keep the risk as low as the feature allows.
1. What exists today (verified)
Reactions. group_message_reactions (the August 2026 reactions migration) holds message_id, user_id, group_id (all
cascade), reaction (a CHECK list of 48 emoji, including 💩 🤢) and created_at, with PK
(message_id, user_id, reaction) and no cap. It is in supabase_realtime with replica identity full. the September 2026 change
removed the UI, and two September 2026 migrations revoked every client grant
(an automated database test asserts it). Rows from August and September 2026 remain.
Deletion. group_messages already has a policy, "Members can delete their own group messages" (a hard
DELETE, in the chat migration of August 2026), but no UI uses it. There is no tombstone column, no edit history and no link
previews. Other places hold copies of a message:
- Reply quotes are frozen copies:
reply_to_excerptandsealed_reply_excerptsurvive a parent's delete. - Chat photos are
group_chat_photosrows referenced from the body. A photo saved to the wall shares its storage object (the chat photos migration). - Phones keep the opened plaintext in a SQLite snapshot, including the overview's
last_message_content. The refetch only ever adds rows. - Pushes are sent by
send-message-notification, which returnsmessage_goneonly when the row is missing. Pushes have no collapse or withdraw id. In unencrypted groups, a push can carry about 140 characters of the message to adult recipients. - Reports:
member_reports.reported_textkeeps the reporter's copy of the message (PIA-007). - Voice: "read me the last message" reads
content. - Backups: up to 90 days.
Live child policy v1.7 already says "You can delete a message you sent" and "Ask an adult in your group to remove
a photo or a message" (legalTexts.ts:853, 877). Neither is true today. PIA-007 says there is no admin delete.
2. Reactions: the data and who sees it
- The data: who reacted, with which of a closed list of emoji, on which message, and when. It is a member's personal information, and a child's under the Code.
- Recipients: active human members of the group only. Walls and profiles neither write nor read reactions (C6). Nothing crosses a group boundary. There are no counts or rankings anywhere except the chips under a message.
- Encrypted and Device-only groups: in a plaintext closed-list design, GroupApp would see "Sam put 😢 on message X at 21:04" for a chat the group chose to keep from us. The policy's list of what stays readable ("names; times, dates, amounts and who does what") does not honestly cover an expressive emoji. So F2 asks whether to seal the emoji in encrypted and Device-only groups, or keep it plaintext with a policy line.
- Children: under-16s react and see reactions on the same terms as adults. An age-gated version buys nothing in
law (self-assessment §5.3; Step 7). The guardian is present in the same chat, because
enforce_guardian_presence(the child-accounts migration) keeps every child account in a group only while a guardian of theirs is active in it. - Notifications: none, ever. A reaction sends no push and no in-app notice, and adds nothing to the unread count. That also keeps clear of s 4A(5)(b) and of Step 7's "promotes engagement through notifications".
- Assistant/Jev, brief, Mail Sifter, LangSmith, analytics, Sentry: none of them reads or records reactions.
- Export and erasure:
export-my-dataadds the table. Account deletion cascades. A deleted message loses its reactions (C9: a tombstone is an UPDATE, so the cascade won't fire). A member who leaves keeps their reactions in the group's record, as with their messages. - Blocks apply on the phone: a blocked member's reactions are left out of the
blocker's chips and counts. Reports copy only the message. The operator's
remove_reported_itemtakes the reactions with it. A reaction can't be reported on its own; the remedies are blocking or reporting the member.
Reuse or replace: replace. Keep group_message_reactions dormant and ungranted, so the September change's "do not re-grant"
comment stays true and the old rows never come back. Add a new table, e.g. group_message_reactions_v2, with a
slot smallint check (slot between 1 and 3) in its PK (message_id, user_id, slot). The 3-per-member cap is then
structural: no trigger, no count, no race. A unique (message_id, user_id, reaction) stops the same emoji
twice. In sealed groups, reaction is null and sealed_reaction holds the emoji (F2), and a database check ties
which column is used to the group's chat-encryption setting. Delete the dormant rows (F6).
3. The legal record: what must change (in the same change as the feature)
The honest position. Reactions are a feedback feature under s 4A(5)(a). That is plain on the words, and the app's own §5.3 already says so. They can only be used with an account, so they make a logged-in feature (s 4A(3)), and s 4A(1) is met on that ground alone. Limb (a)(iv) was already treated as met through the recommender (§15.3), so in that sense nothing changes. What changes is that a second, unarguable route now meets it, so no reading of §5.1 or §15.3 can take the app back outside the definition at the features step. Whether GroupApp is an age-restricted social media platform now turns only on limb (a)(i), its purpose. eSafety's Step 7 tells a service without evidence to presume against itself, and the §7A stocktake still doesn't exist. The §15.9 finding moves from "open, not a risk we accept" to "accepted by the founder, 31 October 2026", quoting their words. "Too small a fish" and "they would contact us first" are the founder's expectation, not part of the argument. The record should say that eSafety has had information-gathering powers since 12 September 2026 (§15.10), and that s 63D now carries 60,000 penalty units.
The strongest truthful position (for the self-assessment's new §16):
- Purpose: the service is for organising. Chat and its reactions coordinate a household, and reactions are a one-tap reply ("✅ got it", "🛒") inside a conversation, not a public metric.
- Closed groups: membership needs an invitation or an approval. There is no discovery, search, public content or cross-group surface for individuals.
- Guardian presence: every under-16 account is in a group only alongside an active guardian (by trigger). Children can't found a group or invite anyone.
- No engagement machinery: history visibly ends ("Start of conversation"), and no feed adds material. Nothing is ranked, recommended or notified by reactions, and no count exists outside the message.
- Evidence: run the §7A stocktake (counts only) and parked lawyer review. Those are what can turn "presumed significant" into "outside at limb (i), on evidence". s 5(a) (messaging) stays unclaimed (§6).
Edits:
- The under-16 ban self-assessment:
- Add a dated §16 re-run as above, and change the §15.9 status.
- Update the reactions row in §5.3 to "reinstated" and add rows to §5.5 and §7.
- Mark §10 item 0 as tripped.
- Add a §12 risk: "a feedback feature is present for every member; the position rests on purpose".
- Correct the stale claim in §5.3, §12 and the AGENTS rule that likes and reactions were "still writable through PostgREST". the 8 September 2026 migration made that structural on the day.
- The privacy posture, §§4.3 and 5: add a dated correction that reactions were reinstated and that the posture is now governed by §16. Fix the same stale "writable" line.
- The child accounts assessment, §5.2 and its residual risks: add a dated correction that the feedback-feature question is reopened at the service level and accepted by the founder. §2.2 already lists reactions.
- This register: add a row for this PIA, update the self-assessment row, and update the guidance on when a PIA is owed.
- The engineering rule "The App Shows Nobody How Others Engaged With What They Posted": rename it to "Engagement Stays Inside The Message: Never Counted Elsewhere, Ranked Or Broadcast" (until 1 November 2026, "…Ranked Or Pushed"). Receipts, likes, seen-by, view counts, thanks counts and streaks stay forbidden; reactions on group-chat messages become the one bounded exception (C2-C7). Sweep the sibling rules that name reactions, and the inventory row.
- The automated "no feedback features" check narrows rather than disappears:
- Keep every receipt, like, seen-by, thanks and streak pattern, everywhere.
- Allow the reaction identifiers only in the chat screen, its message components and the chat store, by a scoped allow-list with its reason, and still forbid them in Social, Dashboard, Widgets, Home, SharedDisplay, Motivation and the rest.
- Add patterns for
mostReacted|topReacted|reactionScore|reactionsReceived|reactionCounteverywhere. - Rewrite the header for the new position.
- Keep the mutation test: a reinstated heart or receipt must still fail.
- Replace the database denial test for the old table with tests for the new table, and keep a denial test on the old one.
4. Deleting your own message
| Item | On delete |
|---|---|
Body: content, sealed_content, wrapped_data_key, key_version |
Sealed columns set to null, which is removal, not hiding (the server needs no key). content becomes the placeholder (its NOT NULL / non-blank CHECK forces one). deleted_at is set |
This message's own quote: reply_to_*, sealed_reply_excerpt |
Nulled |
| Quotes of it in other members' replies | The quoted words are removed from every reply: sealed_reply_excerpt is nulled and reply_to_excerpt becomes the fixed words "Message deleted" (a reply must keep a quote, and the same words show in older apps and where the original isn't loaded); the reply still jumps to the line and shows "Message deleted" |
| Reactions on it | Deleted |
| Chat photo it carries | Its group_chat_photos row and storage are removed, unless it was saved to the wall (shared object, F3) |
| Link previews, edit history | None exist |
| Push already delivered | Can't be withdrawn. No collapse id; iOS would need a notification service extension. Previews only ever reached adults in unencrypted groups. The staged v3.15 already says so |
| Not-yet-sent push | Must treat deleted_at as gone; today it checks only for a missing row (C10) |
| Report copies (PIA-007) | Untouched, and swept on their own schedule. Deleting never cancels a report |
| Assistant: voice "last message" | Skips tombstones. A member's own manual hand-off into their assistant conversation (30-day sweep; LangSmith 14 days if opted in) remains, like a screenshot |
| Mail Sifter, analytics | None hold chat. PIA-005's messages_sent stays a daily count |
| Phone caches | The tombstone overwrites the SQLite snapshot and the overview on the next sync (C11) |
| Backups and PITR | Kept up to 90 days (policy line 561) |
| Audit | Nothing logs a body today, and the RPC must not either (C12) |
The line: it sits in the author's bubble position with their name, as any message would, reading "Message
deleted" ("You deleted this message" for the author). It shows no time of deletion, no body, photo, quote or
reactions, and has no reply, copy or report action. If the author is blocked, it stays hidden like their other
messages. Old app versions render the placeholder content as text, so choose those words with that in mind.
Time limit: none. A delete is the author's act, never a timer. A scheduled or "disappearing" delete would be a time-limited feature under s 4A(6) and must not be built.
Realtime and offline: the tombstone is an UPDATE, which RLS checks before postgres_changes sends it. Clients
replace the row. Offline and backgrounded clients fetch tombstones for their cached window on resume
(deleted_at > last sync), because the merge-only refetch would otherwise keep the body.
Gaps it must close:
seal_group_messagelets any member seal any unsealed row, so a tombstone could be refilled with fake content under the author's name. That function andedit_shared_plan_messagemust refuse tombstoned rows.- Hard deletes skip the line and the quote clean-up, so the direct DELETE grant goes and the only path is the RPC.
- Deletes are allowed in archived event groups. Keep that (it's the author's right).
- Former members can't delete their old messages: residual R3.
- Separate finding: account deletion hard-deletes all of a person's messages in shared groups, with no line, and policy §8 doesn't say so. Fix the policy text in the same edition.
Guardian or admin deletion: the founder asked only for your own messages (F1).
5. Mandatory conditions
C1 Legal record in the same change (§3): §16, the §15.9 status, the corrections, the register, the rule rename and the narrowed guard test. No code merges without them.
C2 Nothing outside the message. No count, total or "most reacted" anywhere: not Home, the wall, the brief, widgets, Photos, export summaries or analytics. Nothing is ordered, ranked or selected by reactions.
C3 No reaction notifications of any kind (push, badge, in-app, email), and no opt-in for them.
C4 Structural cap: a new table with slot 1-3 in the PK and a unique emoji per member per message. Grants are select, insert and delete only. The old table stays ungranted.
C5 Closed vocabulary, kept in the database and approved by Design and Security, without 💩 or 🤢. In sealed groups it's checked on the device (R1).
C6 Policies: insert as yourself, with
is_active_group_member,managed_kind(auth.uid()) is null, the message in the same group and not tombstoned, and the archived-group restriction mirrored. Select needsmanaged_kind is null. Delete is your own rows only. RLS tests cover another group, pending, the wall, a 4th reaction, a duplicate, a tombstoned message, an under-16 (allowed) and MFA.C7 Realtime proven locally. Supabase doesn't apply RLS to DELETE events, and their old record holds only the PK. Either prove that a reaction removal reaches only that group, or send removals through a private broadcast (
realtime.send, ids only, withrealtime.messagespolicies like typing's). A wall must never receive them.C8 Readers: no assistant tool, voice, brief, Mail Sifter, LangSmith, PIA-005 counter or Sentry path reads reactions. A source scan of the assistant server and the server functions allows the table only in export and erasure.
C9 Delete RPC:
delete_own_group_message(id), SECURITY DEFINER, for the author while an active member, and for system notices never. It sets the tombstone, nulls the sealed columns and quotes (its own and others'), deletes the reactions and handles the photo (F3). It is idempotent. Revoke DELETE ongroup_messagesfromauthenticated.C10 Tombstone means gone:
send-message-notification, voice, the wall chat page, overviews and export all treatdeleted_atas deleted.seal_group_messageandedit_shared_plan_messagerefuse tombstones (tested).C11 Device sync: tombstones overwrite the SQLite snapshot, the overview's
last_message_contentand the quotes on resume. Tested offline.C12 No body in logs: neither the RPC nor the client logs content, the quote or the emoji with ids.
C13 Honest copy: the confirm says "Everyone in {group} will see “Message deleted” instead. A notification already on someone’s phone can’t be taken back." (the second sentence is left out only for an author known to be under 16, whose words never reached a notification). A guardian is also told "{Name} will see “Removed by your parent or guardian”.", and a photo saved to the photo wall adds "The photo stays on the photo wall." It never says a message is unsent from anyone's phone.
C14 Policy (F5). In the privacy policy:
- reactions: who sees them, no notifications, not read by the assistant, kept until removed or the message is deleted, and the F2 text for encrypted groups;
- deletion: what is removed, that reply quotes go with it, and what stays (delivered notifications, report copies, backups for 90 days, copies someone made);
- the §8 account-deletion fix.
Child policy v1.8: "You can add up to 3 emoji to a message; everyone in your group sees who added which." Lines 853 and 877 become true, or are reworded per F1.
C15 Review: a Codex round on the migration, the RPC and the policies, then Security reviews the change against C1-C14.
6. Residual risks (accept)
- R1 In sealed groups, the vocabulary and duplicate checks run only on the device. A modified client could store any short sealed blob.
- R2 A member can say something abusive and then delete it. The line stays, and the member can still be blocked or reported. Once a message is deleted it can't be reported as a message.
- R3 Former members can't delete their old messages.
- R4 Delivered notifications, backups, other people's screenshots and manual copies outlive a delete.
- R5 The founder accepts the §15.9 finding: a feedback feature is present for every member, and under-16 accounts continue.
7. Founder questions
- F1 Guardian deletion. May a guardian delete their own child's message? The child policy already promises an adult can remove one. Recommend: yes, for a guardian of that child account only, not other admins, with the same line for everyone and "Removed by your parent or guardian" in the child's own view. If no, reword child policy line 877.
- F2 Seal reactions in encrypted and Device-only groups? Recommend: yes. Otherwise the policy must say that GroupApp can see which emoji each member put on which message.
- F3 A deleted photo message that was saved to Photos. Recommend: the wall copy stays, and the confirm says so and links to it. If it wasn't saved, the photo is deleted with the message.
- F4 The wall shows no reactions and can't add them? Recommend: yes. A hallway screen shouldn't show who reacted to what.
- F5 Policy edition. Recommend: the first edition that ships with the feature (v3.16 unless v3.15 hasn't gone out), plus child policy v1.8, with the §8 fix included.
- F6 Delete the dormant August and September reaction rows (row count recorded with the change)? Recommend: yes. Nobody has seen them since 8 September.
- F7 Accept R1-R5, and approve PIA-016 with C1-C15. Recommend: yes, once F1-F6 are answered.
Founder answers (2026-10-31)
- Who reacted: names, shown only in the long-press sheet; chips on the bubble show the emoji and counts (Design's recommendation).
- F1: yes, a linked guardian may delete their own child's message; other admins may not. Child policy line 877 becomes true.
- Emoji set: the 48 from the original reactions feature minus 💩 and 🤢 (C4); the quick six are 👍 ❤️ 😂 🎉 🙏 ✅.
- F2–F6 as recommended: reaction emoji are sealed in encrypted and Device-only groups; a photo saved to Photos stays there and the confirmation says so; walls show no reactions and can't add them; the policy and child policy v1.8 change in the first edition that ships the feature (with the §8 account-deletion fix); the dormant August–September reaction rows are deleted, with the count recorded with the change.
- Delete: a confirmation, no Undo (Design Q2: an Undo would keep the deleted words).
- F7: residual risks R1–R5 accepted; approved with C1–C15.
Engineering notes (31 October 2026, for the C15 review)
Where C4, C7, C9, C11, C13 and C14 can't be met word for word, the route taken instead is recorded in HQ's engineering scoping notes of 31 October 2026, and Security confirms each at review. For deleting: a reply must keep a quote, so a deleted message's quotes become "Message deleted" rather than empty (C9); reactions are removed from a deleted message by the reactions change (C9); and a phone check is added to the automated offline test (C11).
Security amendment, 31 October 2026 (founder review pending for R6)
Security, 31 October 2026, after the first code review of the deleting change. This adds to §4 and C9 without replacing them.
Why. As first built, deleting removed the photo's record and left removing its stored files to the deleting phone, with nothing to retry a failure. It also chose the photo from an optional id the phone supplied, so leaving it out kept the photo and substituting another removed the wrong one. Neither kept §4's promise that the photo goes with the message.
§4, the "Chat photo it carries" row, now reads: the photo is the one the server bound to the message when it was
sent (group_messages.chat_photo_id), never one the phone names at delete time, apart from the R6 fallback. Unless it was
saved to Photos (F3), its group_chat_photos row is deleted in the same step as the message, and its stored files are
removed by a durable server-side queue that retries until they are gone.
C9, added:
- Binding.
group_messages.chat_photo_idis set when the message is sent. The database refuses a binding unless the photo is a chat photo in the same group, uploaded by the message's author, and not bound to any other message. A wall photo shared into chat is never bound. Once set, the binding can't be changed, only cleared, and encrypting an older message keeps it. In groups with encryption off, the database checks it against the photo link in the text and fills it in for older app versions. Plain messages from before this change are bound once, from their text. A deleted message's binding is cleared. - What the server learns. In encrypted groups, the binding tells the server which message carries which chat photo. It already holds the photo's uploader, group and upload time, and the matching message's author and send time. Nothing about the image, caption or words is revealed. This is "who does what" information, which our privacy policy already lists as readable. No change to the policy text.
- Removal queue. Deleting writes one
group_chat_photo_removalsrow (photo id, group id, storage location copied from the deleted photo record, times, an attempt count). It holds no person, message or actor, and is not linked to the rows it outlives. Only our server can read or write it. Our hourly photo clean-up drains it: files first, then the row is marked done; on any error the row stays pending and is tried again. The deleting phone's own immediate removal is a speed-up only. - Retention. A pending row stays until its files are confirmed gone and never expires; one that keeps failing is reported as a count. A done row is deleted 90 days after it is done, matching backups. The row names no person, so account deletion and the data export need nothing.
- Logging (C12). No photo, message or group id, storage location or file path in any log, notice, response or error report from deleting, the clean-up or the app. The clean-up reports counts only.
R6 (new residual risk; founder: accept or reject). Encrypted photo messages sent by an app that couldn't bind it (before this change, or from a phone not yet updated) can't be bound by the server, because it can't read which photo they hold. For those messages only, the app may name the photo when deleting, and the server accepts it only if every check passes: the message is unbound; the photo is unbound, in the same group, uploaded by the author, no later than the message and no more than 24 hours before it. There is no cut-off date, so a message from a phone that updated late is covered too (Security review, 31 October 2026). If the app names no photo, or one that fails a check, the message is still deleted but its photo stays on the server, still readable by the group's members, until its uploader deletes it from the photo viewer. The same checks mean the worst a modified app can do is remove one of the author's own older unbound photos. If the founder rejects R6, the alternative is a one-off "finish removing old photos" step run on a group member's phone, which can read the sealed messages.
As built: the organising set (1 November 2026)
- One per member (A1): the primary key is
(message_id, user_id)and the slot column is gone, so a second reaction has nowhere to go but in place of the first.set_group_message_reactiondeletes yours and inserts the new one in one transaction, as the member (not as the database's owner), so every check on adding one still applies, and a refused change leaves the old one standing. Taking one off is deleting your own row, as before. - The vocabulary (A2): the database accepts exactly 8 stored values,
got_it,got_it_15m,got_it_today,got_it_tomorrow,done,cant,need_handandthanks, and refuses every emoji. Rows from the 31 October build existed only on integration and development databases, never production, and were deleted. - Sealed at one length (A3): in an encrypted or Device-only group every value is padded to 16 bytes before it is encrypted, so all 8 seal to the same 76 characters and the server can't tell them apart by size.
- Words, never values (A4): the sheet shows each answer as its icon and words, and screen readers read the words. The sheet's "Reactions" list says who chose what ("Sam: Got it, in 15 min"). Chips on the bubble show the icon, and a count from 2; every Got it, whatever its time, is one 👍 chip.
- Unchanged: who can react and see reactions (C6, C7), walls and profiles, archived groups, tombstones, blocks, the private poke, sealing older reactions, the key rotation and lock, and logs (C12). (Notifications: see the next section.)
As built: the author's notification (1 November 2026)
- Ids only, read at send time (A12): adding a reaction makes the database post the group, message, reactor and reaction ids and the time of the reaction, and nothing else, to the message notification function; taking one off, sealing older ones and the key rotation post nothing. The function reads the reaction and the message again when it sends, and sends nothing if the reaction is gone, the message is deleted or more than 7 days old (F-c), or it is the reactor's own message.
- The author only (A5, A9): the one recipient is the message's author, an active person in the group (never a wall or a profile), and the reactor must still be one too. Nobody else is ever sent it, so a guardian gets no copy.
- Blocks and mute (A10, A11): nothing if either has blocked the other; a failed block lookup sends nothing. A muted chat gets nothing; a failed mute lookup sends the generic body, as for a message. The phone's open-chat banner gate treats it as a chat push for that group.
- What it says (A6–A8): the title is the group's name. Every preview gate true (an unencrypted chat and row, the
reactor allowed as author of the words and the author as recipient, the switch on, a phone that reported the
private channel): "Sam: 👍 Got it, in 15 min · “Can you pick up Mia…”", the quote at most 40 characters and cut at
a word. Otherwise "Sam responded to your message"; Device-only (or an unreadable level): "Something changed in
". The server opens nothing: a sealed reaction has no words, so an encrypted group never learns more than that a reaction exists, which it already could. Thanks notifies like the others, so there is no notify flag. - The limits (A14, and the reviews before release): at most one to an author per group every 10 minutes, and at most 30 an hour from one person reacting in one group. Each reaction is decided once, so a repeated delivery of the same reaction never pushes twice, and a reaction more than 23 hours old never pushes. Two kinds of row decide this: an author, a group and a time; and a reaction's id and time, its group, who reacted, when it was decided, and whether it was pushed. Neither holds an emoji, a message or a name; no app role can read or write them, and both are deleted before they are a day old. The next push after a quiet window names who else responded to that message in between: "Sam and Ada responded to your message", or "Sam and 2 others". A reaction inside the window that nothing follows is not pushed later: the author sees it in the chat.
- A switch that stops them (before release): a server-side switch stops every reaction notification, whatever it would have said, without a release. It is off unless set exactly on, and an unreadable switch counts as off.
- No badge, no count (A15): the push carries no badge, asks for no unread count and changes none. A source scan fails a second path, a second recipient or a badge.
- Logs (A16): fixed codes and error codes only; the reply is a count or a code; the delivery record gains no column and records the push as a message push, with no subject, so it never stands in for the catch-up notification about unread messages.
As built: deleting (31 October 2026)
- One way to delete:
delete_own_group_message, for the author while an active member, or a linked guardian of an author who is under 18 (or has no birth month on record). Never an owner or admin as such, a wall or a profile, a system notice or an event-confirmation row. Every refusal gives the same answer. The direct delete members held is withdrawn. - The tombstone is a database rule: once
deleted_atis set, the row holds the fixed words "Message deleted" and nothing else of the body, the sealed copy, its key or its quote. - Quotes of it (differs from §4's table, which said nulled): a reply has to carry a quote, so every reply's quote is replaced with "Message deleted" and its sealed copy removed. The deleted words are gone from the server either way, and the reply still jumps to the line. A reply sent afterwards by an older app gets the same words.
- The photo (F3): the one bound to the message when it was sent, removed with the message unless it was saved to Photos, which keeps it; its files are removed by the queue in the Security amendment above, whatever the phone does. A phone that held the photo drops its own decrypted copies and caption when it learns of the delete.
- Guardian removals: a separate record only the child can read, so their phone says "Removed by your parent or guardian" and nobody else learns who removed it.
- Readers: notifications not yet sent, voice "last message", the export, the unread count and the catch-up notification treat a deleted message as gone; sealing and plan edits refuse one; it can't be reported as a message, and a report made before is untouched. Phones replace the row, the quotes and their saved copy when they next sync.
- Not changed here: reactions (next change, which also removes them from a deleted message), the policy text, and account erasure, which still removes all of a person's messages without a line or quote clean-up (HQ to decide).
Sources
supabase/migrations/20261101184148_reaction_organising_set.sql— one per member, the 8 values, the sealed shape,set_group_message_reaction.The reaction push migration,
supabase/functions/send-message-notification/(handler.ts,payload.ts,__tests__/reaction.test.ts) — the author's notification.components/Chat/__tests__/noFeedbackFeatures.test.ts— the scan that keeps it the one push, to the author, with no badge.__tests__/chat_reactions_rls.test.ts,lib/__tests__/chatReactions.test.ts,store/__tests__/groupChatReactions.test.ts— the organising set's tests.supabase/migrations/20261031140919_chat_delete_own.sql— the tombstone column and its shape,delete_own_group_message, the guardian removals table, the reply trigger and the readers restated.__tests__/chat_delete_own.test.ts— the database tests for every condition above.lib/chatDelete.ts,constants/chatPrivacy.ts,store/slices/createGroupChatSlice.ts,components/Chat/MessageRow.tsx— the phone's half.supabase/functions/sweep-photo-copies/sweep.ts— the clean-up's removal of deleted chat photos' files.docs/pia/2026-09-ban-self-assessment.md,docs/pia/child-accounts.md,docs/pia/member-safety-reports-and-blocks.md,docs/pia/notification-previews.md— the assessments this one amends.