← 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.

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:

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

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):

  1. 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.
  2. Closed groups: membership needs an invitation or an approval. There is no discovery, search, public content or cross-group surface for individuals.
  3. 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.
  4. 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.
  5. 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:

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:

Guardian or admin deletion: the founder asked only for your own messages (F1).

5. Mandatory conditions

  1. 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.

  2. 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.

  3. C3 No reaction notifications of any kind (push, badge, in-app, email), and no opt-in for them.

  4. 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.

  5. 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).

  6. 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 needs managed_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.

  7. 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, with realtime.messages policies like typing's). A wall must never receive them.

  8. 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.

  9. 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 on group_messages from authenticated.

  10. C10 Tombstone means gone: send-message-notification, voice, the wall chat page, overviews and export all treat deleted_at as deleted. seal_group_message and edit_shared_plan_message refuse tombstones (tested).

  11. C11 Device sync: tombstones overwrite the SQLite snapshot, the overview's last_message_content and the quotes on resume. Tested offline.

  12. C12 No body in logs: neither the RPC nor the client logs content, the quote or the emoji with ids.

  13. 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.

  14. 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.

  15. 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)

7. Founder questions

Founder answers (2026-10-31)

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:

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)

As built: the author's notification (1 November 2026)

As built: deleting (31 October 2026)

Sources