← All privacy impact assessments

PIA: cross-group events — RSVP invitations and event groups

Amended 30 October 2026: deleting a group. An owner can delete a group. Members are told at once, and the group is deleted seven days later unless the group's owner cancels; meanwhile anyone can export their own data or leave. Deleting removes everything the group holds for everyone in it, children included: messages, photos (sealed or not), lists, routines, baby records, the group's inbox and its attachments, and its keys. Our backups expire within 90 days and a deleted group is not restored from them. A star rating a member gave stays, with no group attached. Copies of the group's events in connected groups say the group no longer exists.

Status: Built. RSVP invitations and event groups both shipped 8 September 2026; the event-group photo wall shipped 9 September 2026. Written: 7 September 2026 · Last revised: 30 October 2026 (deleting a group); 14 October 2026 Nothing here is legal advice. Children's Code citations are to the exposure draft.

This is the first feature in GroupApp's history that would let two groups touch. That is the whole reason it needs an assessment before a schema: the absence of any cross-group surface for individuals is doing real work in the ban self-assessment, and it is the property this feature spends.

Amended 14 October 2026, before the code: an event group's history reaches everyone who joins it. From the app update released after 14 October 2026, members' phones send a group's earlier keys to every person's phone in it, so a member can read what was written before they joined (the key architecture assessment, §3.7). Event groups follow the same rules as every group. Their first keys are made after the change that gave keys a check value, so sharing is on from an event group's first key (made while its host is the only member), with no notice in a group that has nobody else to tell; each adult joining is told before they join (under-18s still cannot join an event group, §6). This means an adult from another family who joins an event group late can read its earlier chat and see its earlier photos, which can include other families' children. We accept that (§13): it is what lets a late joiner catch up on the arrangements, and the photos were posted to everyone in the event.


1. Purpose and scope

What it is for. A birthday party. One family is hosting, five others are coming, and today the app has nothing for it — the invitation goes out by text message and the RSVPs come back in six different threads. The feature is a calendar invitation that crosses a group boundary and comes back answered, plus (Step B) a temporary group for the coordination a bigger event needs, and (Step C) persistent connections and direct attendance for own groups.

Step A — RSVP invitations. A host issues a code for one of their events. An adult in another group redeems it, picks which of their groups' calendars it lands on, and answers. The event is mirrored into that group as an ordinary calendar row.

Step B — event groups. groups.kind = 'event': a temporary group with chat, a bring list, the one event, and members. Nothing else. It goes read-only on a date, is announced as going read-only on that date, and is kept as a record of the event until an admin of the host group deletes it. and is deleted. — the draft said deleted on a timer; see §1B.

Step C — connected groups and direct attendance. Added 24 September 2026. Bilateral connections between groups, targeted event invitations, and direct attendance for an adult's other groups. Assessed in detail in the connected groups assessment.

1B. Step B as built (2026-09-08)

We changed the lifecycle before this was built, because of the app's legal position. The draft (§7, below, as first written) said an event group is deleted thirty days after it goes read-only, chat and tasks included, and treated that as a privacy virtue. It is also, almost verbatim, the time-limited-content limb of the logged-in-feature definition in the Online Safety (Age-Restricted Social Media Platforms) Rules 2025 s 4A(6) — "enables viewing of material only within a limited period after it has been posted" — and this app's whole position under the under-16 ban is that it satisfies NO s 4A feature. A deletion timer on a chat would have put the app back inside the condition through a feature nobody thought of as social at all.

So: an event group archives seven days after its event, and is never deleted by anything but an explicit act. No delete_after column, no deletion sweep, and no copy that promises otherwise. The ban self-assessment §5.4 records the same decision from the other side.

This is the second time this register has changed the product rather than described it (the first was the feedback features, §5.3 there), and it is worth noticing that both times the change came from taking the statute's own words seriously against a design that read well.

Draft said Built Why
groups.kind in ('group', 'event') ('standing', 'event') "Group" as a value of a column on groups says nothing; our later decision that every group is called a group, not a household, made group the word for every group, so the kinds needed a word that distinguishes them
delete_after, and a sweep that "archives and deletes" No such column. sweep_event_groups sets archived_at and has no delete statement in it The s 4A(6) argument above
Read-only "enforced by a restrictive policy on writes, not by the client" Built exactly so — as restrictive INSERT/UPDATE policies on group_messages, group_tasks, group_schedule_events and task_completions, keyed on is_archived_group(group_id). Reads untouched An archive nobody could read would be a deletion with extra steps
"One as restrictive policy per tool table" 29 tables, plus a join-predicate policy for group_photo_reports — the baby domain (11), meals (5), games (8), voice (2), photos (2) and chat_sessions. Proved by an adult member of an event group attempting a representative insert into each bucket, AND by the same insert succeeding in their own standing group The second half is what makes it a test of the KIND rather than of some unrelated refusal
— A restrictive policy is NOT enough for the baby domain, and BEFORE INSERT TRIGGERS were added for it. Nothing in this schema has force row level security, and every SECURITY DEFINER function is owned by the table owner — so log_baby_event, log_feed, log_past_feed and request_baby_history_deletion skip a policy entirely. Triggers on group_babies, baby_logs, baby_feed_sessions and baby_history_deletions fire for the group owner too This is the structural-vs-conventional distinction this assessment exists to catch, found by asking the question rather than by a failure. For the rest of the list the gap is academic — an RPC would have to be handed an event group's id by hand to write a row nothing renders — and it is stated rather than closed, which is the honest position
— The free-collection triggers were patched, not policed. Creating a group fires an after insert trigger that grants every free collection; SECURITY DEFINER, so the policy would not have stopped it, and an event group would have silently held the whole meal catalogue. Three body-only replacements add where kind = 'standing' A restrictive policy that a trigger walks past is the appearance of enforcement
— can_access_group_media_object gained the same predicate. The photo BUCKET is authorised by parsing the object's own name, so a policy on group_photos would have left the objects writable Our rule that a photo bucket is authorised by the object's own path, not by its metadata row: the metadata row and the object are two separate write paths, and both need guarding
— The four new groups columns are refused to every client write by a trigger under a transaction-local arm, the same shape already used for the RSVP mirror columns. Active members can update groups would otherwise let any member flip an event group to standing and re-enable every tool, or clear archived_at and write into an archive The permissive UPDATE policy on groups is deliberately wide (any member may rename a group); the guard is per-column
Joining is "the existing invite code and pending-then-approve ceremony, unchanged" Unchanged, with two refusals added inside request_join_group (a body-only replacement, since the earlier migration may not be edited): an under-18 cannot ask to join an event group, and an archived event group admits nobody. Neither is charged to the guess ledger — the caller was holding a real code The §2 rule, server-side, proved with an under-18 account. The check is scoped to event groups: a child joining their own family's group is untouched
"Approval also creates the mirrored event in the joiner's chosen group" group_members.event_home_group_id records the choice at join time (set_event_group_home, the caller's own row only); an after update of status trigger creates the mirror when they are approved. Null means no mirror — they still see the event inside the event group Which calendar is the joiner's decision and nobody else's, and a pending member has asked rather than been let in. An event landing on a family's calendar before anyone said yes would be the invitation letting itself in
— The founding group gets a mirror too, at creation Without it the household hosting the party is the one household with no calendar entry for it
— The invitation an event group carries has a random code that is never surfaced. create_event_invitation (correctly) refuses an event that lives in a non-standing group, so the row is written directly; it exists as the vehicle the mirror machinery acts on, and the code guests type is the group's own reusable invite Two codes for one party would be a worse feature than the one it replaces
Deletion by "an explicit admin action" delete_event_group requires is_group_admin(parent_group_id) — an owner or admin of the HOST group, deliberately not the event group's own owner, who may be an ordinary member of the host group. A restrictive DELETE policy on groups says the same thing to the direct client path The space belongs to the group that asked for it
"No group-specific assistant … our assistant server re-reads groups.kind on every request" Built, at both doors and in three places: a function called buildAgentContext calls is_standing_group beside the membership check and fails closed, on both the assistant's text endpoint and its voice ask endpoint; voice's per-turn groups read carries kind and refuses before dispatch, which is what covers reading the group's chat back aloud; and the voice pairing endpoint refuses too. A source scan of our assistant server's own code is what proves it Reading a group's chat back aloud is the one thing that would otherwise read another family's party chat to whoever is in the room. It never reaches buildAgentContext, so a single gate there would have missed it
— The dashboard brief is off for the kind, by passing a null group id It is a model writing about the group, which is what "no group-specific assistant" means
— voice_grants is on the restrictive list, so a grant naming an event group cannot exist at all Our rule that a voice grant names the group it applies to, so a grant naming a group nobody may see is not a smaller version of the problem — it is the problem

Built later, and re-assessed (9 September 2026): the event-group photo wall (§8). Until the photo-encryption work shipped, group_photo_albums / group_photos were on the restrictive list precisely so it could not arrive by accident; an event group now admits a photograph only when it is sealed on the device (encrypted = true).

In scope: everything that crosses the boundary, in both directions; who may cross it; what each side learns about the other; the lifecycle and deletion of an event group; and a re-run of the ban self-assessment, because this feature changes the answer's inputs.

Out of scope: the sealing of photographs itself, which the photo wall assessment covers; the event-group photo wall is designed in §8 and was re-assessed there as built on 9 September 2026, with the photo-encryption work; the child-account design itself (see the child-accounts assessment); the universal invite link, which is a transport decision, not this feature.

1C. Revision, 25 September 2026: the group's answer and each member's attendance

Accepting an event from another group used to put it on every member's schedule. It now records two separate facts:

Who can change attendance. A member can change their own. A group admin can change anyone's. A guardian can change their own under-18 child's. All three go through one server action; the database refuses any other change to attendance on a copied event, so a broad edit to the event row cannot be used to change somebody else's answer.

Where it shows. An accepted event appears on the Schedule only for members marked as going. Members who said no, or have not answered, still see the event, their answer and everyone else's in the group's Other Groups Events tool, which also lists declined events (which have no schedule entry at all) and pending proposals. That tool is shown to adult members only, as every other cross-group surface is.

What this does not change. The host still learns only the responding adult's name, the headcounts and the note — never which members are going. Attendance is a coordination fact within one group, in the same way as an ordinary event's RSVP; nothing counts or ranks it.

The tool and the assistant use the same server actions. The assistant can list these events, redeem an event code, add an event to another of the member's groups, answer a proposal, and change attendance within the same permissions. It does not repeat an event code in its replies.

1A. Step A as built (2026-09-08)

Everything in §§3–6 and §10 that concerns Step A is now a description of the code, not a plan. Where the build departed from the draft, the departure and its reason:

Draft said Built Why
An RSVP per responding person One answer per responding GROUP — unique (invitation_id, responder_group_id); a second adult in the same group replaces the first answer; one mirror per group; max_groups counts groups that accepted Two parents redeeming the same code are the same family answering. Two rows would have been two headcounts for one family and two copies of the party on one calendar
The host "sees" the responder's display name responder_name is frozen onto the RSVP row at write time, and the host reads through list_event_responses, a SECURITY DEFINER RPC returning name, status, headcounts, note and when — a closed list. event_rsvps has a SELECT policy for the responding group only; the host has no row-level read at all The host cannot read the responder's users row (different group — correct), so the one fact they are meant to learn is copied. The responder's user id and group id never reach the host, structurally
The redeem RPC refuses an under-18 Preview, respond and create all refuse an under-18, each with code: 'under_18', before anything is read and without charging the limiter A child should not see another family's event details from a code either. They see the party on their own calendar once a guardian accepts — as an ordinary row, proved in our database tests with a child's own session
cross_group_events consent recorded "to the consent ledger" A USER consent (CONSENT_SUBJECT.cross_group_events = 'user', changed from 'group'), recorded from the accept screen when the person sends their first answer, after a sheet with the notice and two equal buttons §10 already said the consent is always an adult's. A group consent is admin-writable by policy, which would have stopped every non-admin adult from answering
Codes "stored hashed" sha256 of the normalised code, with an honesty note in the migration: 32^6 ≈ 1.07 billion is brute-forceable offline in minutes by anyone holding the table. The hash defends against a glance and against a code in a log or a backup diff; the per-account limiter is what makes guessing through the API expensive Saying "hashed" without saying this would be the overclaim this register exists to catch
"Codes are never logged" — a convention A source scan, run as a test, over every file that holds a code and over the migration's raise statements The cheap upgrade §9 asked for
The host removes a response Removal CANCELS the mirror (cancelled_at), in the same words as a host deleting the event; the RSVP row goes An event vanishing from a family's calendar because the host pressed a button is the failure cancelled_at exists to prevent, whichever button it was
— The two link columns (linked_invitation_id, cancelled_at) are refused to any client write by a trigger; only the RPCs and triggers, under a transaction-local arm, may set them Without it a member of the responding group could point one of their own rows at any invitation id and receive the host's edits, or un-cancel a cancelled mirror
— A mirror keeps the host's zone when the responding group relocates: the group-timezone re-anchor trigger skips rows with linked_invitation_id The party happens at the host's 2pm wherever the guests live; re-anchoring it would move it
— The mirror is assigned to every active member of the responding group at the moment of acceptance (the shared-dinner convention) Superseded 25 September 2026 (§1C): the accepting adult chooses who is going A group accepting and every member attending are different facts; recording them as one put the event on the schedule of people who were never going
— is_standing_group(group_id) — true for every group today; event groups replace its body with groups.kind = 'group' So adding event groups is a one-function change, not a search for every place that should have asked
— Erasure: event_rsvps.responder_user_id cascades. An erased account takes its name and answer with it; the mirror survives as the responding group's own calendar row The name on the row is personal data of the person erased; the calendar row is the group's record

What each side learns, as built, is exactly the §5 table, and a test we run against the database asserts the key list of both closed returns rather than trusting the prose.

2. The design rule that comes before the schema

A child's interaction graph never leaves their guardian's groups. Anything cross-group that touches a child is addressed to an adult in the child's group and answered by that adult. Adults may interact across groups directly.

This is how parties already work for children in the physical world: the invitation goes to the parent, the parent answers. Building it that way costs nothing in realism and keeps every under-18's experience provably group-only.

The boundary is 18, not 16. Under-16 is the ban's line and 18 is the Code's definition of a child; for a brand-new feature that creates a cross-group surface for the first time, the Code's line is the one to take. A seventeen-year-old RSVPs through a parent. That is more restrictive than the in-group variant, which stays at 16, and the asymmetry is deliberate rather than an oversight.

Three lines not to cross, whatever this grows into. Each of them, crossed, moves the service toward the definition in the ban assessment in a way a regulator weighs by actual use:

  1. No directory or discovery of people or groups. Ever.
  2. No cross-group messaging for children.
  3. No child-to-child link of any kind.

3. Data involved

Kind Whose Children's? Sensitive? Free text?
event_invitations.code_hash — the invitation code, stored hashed (sha256; brute-forceable offline from a dump — §1A) the host group's no no no
event_rsvps.responder_name — the responding adult's display name, frozen at write time the responding adult's no no no
Event fields crossing the boundary: title, start_local_time + timezone, duration, place the host group's often yes — "Ada's 8th birthday" names a child no yes — a title is free text
event_rsvps: responder user, responder group, status, adult headcount, child headcount, note the responding adult's headcounts are about children, unnamed unless the note names them possibly — an allergy typed into the note is health information yes — the note
The mirrored group_schedule_events row in the responder's group the responder group's copy of the host's event as above no yes
groups.kind, parent_group_id, ends_at, archived_at (no delete_after — §1B) the event group's no no no
group_members.event_home_group_id — which of a joiner's own groups the party should land on the joiner's, about their own groups no no no
Event-group chat and bring-list tasks adults from several groups no — children are never members no yes

What is deliberately not collected, and is the more interesting list:

4. Flows and locations

Data Device Supabase (Sydney) Crosses a group boundary? Model provider
The plain invitation code exists only in the share sheet on the host's device only as a hash yes, out of band (the host sends it however they like) no
Preview (title, time, place, host's first name) shown to the redeemer via preview_event_invitation, rate-limited per account yes — before any relationship exists no
The RSVP (name, headcounts, note) typed by the responding adult stored yes, to the host no
The mirrored event rendered in the responder's group ordinary group_schedule_events row yes, host → responder yes — it is an ordinary row, so the responder group's own assistant can see it, exactly as it sees any event
Event-group chat stored it is the boundary no. Never read by any model — it is other groups' adults talking

The mirror carries the host's start_local_time and timezone, so the existing dual-storage trigger derives the same instant and each group renders it in its own zone. That is a correctness decision, and it also means no new time-handling code exists to get wrong.

Two model-exposure decisions worth stating explicitly:

5. What each side learns, and what it does not

The host learns The responder learns
Identity The responding adult's display name The host's first name — and, once their group has answered, the host group's name (below)
Content Headcounts (adults, children) and the responder's note Title, time, place
Never The responder's group name, roster, or any other member; any child's name unless typed into the note; whether the responder is on the app before the code is redeemed Who else was invited; who else accepted; the host's group roster; the host's notes on the event (host-controlled, not mirrored)

The preview is the sharpest edge in the design, because it is the one call that returns information about strangers to someone with no relationship to them. Four things bound it:

A forwarded code is the accepted failure mode. A code passed on reaches someone the host did not choose. Bounds: max_groups caps redemptions, expires_at defaults to the event's end plus a day, the host can revoke the code and remove a response, and the preview reveals only what a paper invitation would. For event groups, joining keeps the existing pending → approve ceremony, because approval grants chat membership and that is a higher bar than seeing a date.

The host group's name reaches the responder only after their group answers. The preview, before any relationship exists, still shows the first name alone. Once a group has answered — accepted or declined — its adult members see the host group's name beside the event in Other Groups Events, the same way an event from a connected group is shown, and that group's assistant reads it when it lists those events. Children never see it: that list refuses an under-18 before reading anything. We decided this on 10 October 2026: a family with several invitations needs to tell "the Smiths" from "Jo", and a paper invitation is usually signed by the family. The app had been showing the name since 25 September, and this assessment said otherwise until 10 October. The host is told on the screen where they share the code. The residual is in §13.

6. Under-18s: what the rule looks like in the database

7. Event groups: caps and lifecycle, stated on screen

The caps are a privacy design as much as an abuse control: a temporary group that quietly became permanent is a cross-group social space, which is the thing this app is not.

Default Built
Active event groups per founding group 3 create_event_group, counted over archived_at is null — an archive is a record, not a live space, so a household that has thrown four parties is not locked out by the first three
Members 50 the invitation's max_groups; the group's own invite cap is the ordinary one
Read-only from the event + 7 days sweep_event_groups, nightly at 03:20 UTC
Deleted 30 days after that Withdrawn before this was built, 8 September 2026 NEVER deleted on a timer. Kept as an archive until an admin of the host group deletes it — §1B, and the ban self-assessment §5.4

Stated in plain words on the create screen and the join screen, from one function (eventGroupLifecycleCopy), and the group's card in My Groups shows both dates. Somebody joining a temporary group is entitled to know before they type anything what it holds, when it stops taking writes, and what happens to it afterwards. The third sentence is the one that would have been wrong, and a test we run scans every string the feature can render for a promise of automatic deletion.

8. The photo wall in an event group — built 9 September 2026

Re-assessed 9 September 2026, as built. The three rules below were written when this was a deferred design; each is now either held by the database or explicitly not, and this preamble says which. The detail of the sealing itself is in the photo-wall assessment, revised the same day.

The rule as designed As built Structural or conventional
Ships only once photo encryption exists, so no event group holds an unencrypted photograph Event groups' two restrictive INSERT policies on group_photos / group_photo_albums are replaced, not dropped: an event group admits a row only where encrypted = true, and the bucket function admits an event group's objects only under the sealed/ prefix Structural. Proved both ways in our test suite for photo sealing — an unsealed row is refused, a sealed one admitted
A copy is a new object, re-encrypted on the copying device, carrying source_photo_id and a provenance line Built. copy_group_photo is SECURITY DEFINER solely so the source_photo_id is a photo the caller can genuinely see; the bytes are decrypted, re-sealed under the target group's key and uploaded by the device. The provenance label is itself sealed under the copy's own data key Structural for the link and for "the server never sees plaintext"; the label's wording is the client's
The poster's delete offers "also remove the copies", on by default Built as a durable tombstone: request_photo_copy_removal hides every copy in the same statement (a restrictive SELECT policy on copy_removed_at) and files a group_photo_copy_removals row; sweep-photo-copies removes the objects and then the rows Structural for the hiding; the byte removal depends on a scheduled sweep being wired up
Account erasure removes copies of that person's uploads the same way Built into delete-account, in the same pass as its own storage sweep Structural
Copies survive the event group's own lifecycle, and the copy button says so Built. An event group is archived, not deleted, seven days after the event (§5) and only a host-group admin deletes it; a copy is an ordinary photo of the copier's own group from the moment it lands Structural, and the copy sheet says it in words
A destruction request about a child's photograph is honoured by the poster or a group admin, not by us Built as far as code can build it: reports reach that group's admins who did not post the photo (Manage Group → Reported photos, a marker on the wall and a push), and no service-role reader of reports exists anywhere. The written procedure for this is still owed — this assessment does not close it Conventional, and the honest word is conventional

What the build added that the design did not say. Reporting had to change with it. A report used to be addressed to us; with the photograph sealed we cannot look at it, so a report now goes to that group's admins, and the reporter is still never named (the admin-facing function does not select reported_by). That is a real reduction in what we can do about a bad photograph in an event group, and it is assessed in the photo-wall assessment §8 rather than hidden here.

The original text follows, unchanged.

We decided event groups get a photo wall, and guests may copy a photo to their own group's wall — provided it holds up against deletion rights. It does, on three rules, and it ships only once photo encryption exists, never before, so that no event group ever holds an unencrypted photograph of another family's child.

  1. A copy is a new object, re-encrypted on the copying device under that group's key, carrying source_photo_id and a provenance line — "from Maya's party, posted by Sam". The server never sees either plaintext.
  2. The poster's delete offers "also remove the copies", on by default. The provenance link makes a cascade possible without the server reading anything. Account erasure removes copies of that person's uploads the same way. Copies survive the event group's own scheduled deletion, which is the point of copying, and the copy button says so.
  3. A destruction request about a child's photograph is honoured by the poster or a group admin, not by us, because with the photo encrypted we cannot identify it. The written procedure for this must say so in plain words rather than promising an operator search that end-to-end encryption makes impossible. An honest limitation stated is better than a capability implied.

The residual, stated: a copy lands on a group wall where that group's own children may see it. That is the same exposure as any photograph their own parent posts, and it is the reason children are never members of the event group itself.

9. Risks and mitigations

Risk Who is harmed Mitigation Structural or conventional Residual
The preview RPC becomes an enumeration oracle anyone with an account Closed return list, per-account rate limit, hashed codes, no email or user lookup anywhere in the app Structural A determined attacker with many valid codes learns those events. Bounded and accepted
A forwarded code fills an event with strangers the host, their guests max_groups, expiry, revocation, host removal; approve ceremony for event groups Structural Accepted — §5
A child interacts across a group boundary the child RPC refusals on preview, redeem and create (built) and on join (planned), checked server-side, proven with an under-18 account Structural (preview, redeem and create built and proved; join planned) —
Another family's child's name reaches a model in a mirrored event title that child A planned change routes group data to Australian inference; free text is not filtered Conventional until then Accepted and must be stated in copy. A title is free text and the app cannot know it names a child
A note carries health information — "Ada is anaphylactic to peanuts" that child Nothing structural. It is exactly the information a host needs — Accepted. The note field's copy should not invite more than is needed
An event group becomes a permanent cross-group social space the app's ban position; the children in those groups Caps, lifecycle, restrictive policies, no assistant, no children Structural Accepted
Codes in logs anyone holding a code "Codes are never logged" — a source scan over every file that holds a code and the migration's raise statements (a test we run) Structural for the app's own code (a test); still conventional for any future edge function or landing page Accepted. The scan's file list must grow with the feature
A responding group forges a mirror or un-cancels one the host, the guests The two link columns are refused to any client write by a trigger; only the RPCs and triggers, under a transaction-local arm, may set them Structural —
The hash is mistaken for protection of a leaked table anyone holding a code The migration says in words that 32^6 is brute-forceable offline; the limiter is the real defence against guessing through the API Conventional (a stated limitation) Accepted. A leaked table is a leaked list of live codes until they expire
A copied photo outlives the subject's family's control a child in the photo Provenance link, cascade delete on by default, admin honour of a destruction request Provenance is structural; the honouring is conventional Accepted, and disclosed — §8
A host deletes the event and it silently vanishes from six families' calendars the guests A host delete sets cancelled_at on the mirrors and the card says so; a host removing one group's response does the same Structural (built, proved) —

10. Legal basis and consents

11. The ban self-assessment, re-run for this feature

The ban self-assessment concluded the app is not an age-restricted social media platform, on limb 63C(1)(a)(i) alone. This feature is the one most capable of undermining that conclusion, so the test is re-run rather than assumed.

Element Before With this feature
(a)(i) sole or significant purpose is online social interaction Arguably not — organising Weakened, and the honest word is weakened. A party is a social occasion, and a feature that connects families is closer to social interaction than a chore list is. The counter is that what crosses is a calendar invitation — a date, a place, an answer — and that an event group is a coordination space with a bring list and a deletion date, not a place to hang around. Still arguable; less comfortably.
(a)(ii) link to or interact with some or all other end-users Yes Yes. Unchanged in substance: adults can now interact across a boundary, but only with someone who handed them a code. There is still no way to find anyone.
(a)(iii) post material Yes Yes
(a)(iv) Rules s 4A condition Yes, via feedback features Yes. Unchanged.
Rules s 5 exclusion None None

What preserves the argument, and must be preserved with it:

What would break it: any people search; any "groups near you" or suggested-events surface; any persistent cross-group space; any child able to redeem, join or message across a boundary; any ranked or endless feed of events. Each of those is a normal-looking product idea and each one moves this feature from "a calendar invitation" to "a network".

12. Best interests of the child (exposure draft s 10)

What children get. Their family's social life is legible on their own calendar. A child sees "Maya's party, Saturday 2pm" the same way they see soccer training — which is the correct amount of involvement for a child in an arrangement their parent made.

What they are spared. They cannot be contacted by an adult from another family, they cannot be added to a group chat with children they have not met, they cannot be enumerated by anyone outside their own group, and their name reaches the host only if their own parent types it.

The design cost, and who bore it. A seventeen-year-old cannot RSVP to their own friend's party in the app, which is patronising and they will notice. The Code's line was taken over convenience for a brand-new cross-group surface, on the reasoning that the first version of a boundary-crossing feature should be the restrictive one — loosening later is a decision someone makes deliberately, and tightening later takes something away that people have started using. This is the item most likely to be revisited, and revisiting it is legitimate.

Where a child's interest and the host's diverge: the host wants to know who is coming and whether anyone has an allergy; the child's interest is in not having their name and health information typed into another family's app. The parent decides, in a free-text field, having been shown what the host will see. That is the right person to decide, and the notice at first redemption is what makes it an informed decision rather than a default.

13. Residual risk we accept

14. Review trigger

15. Sources

Primary: Online Safety Act 2021 Part 4A and the Rules — see 2026-09-ban-self-assessment.md §14, where they are quoted and the verification is recorded. Children's Code exposure draft ss 10, 14–17, 32, 38 — via docs/privacy_posture.md §4.2, not re-read for this document.

In repo: our own rules that a rate limiter which raises an error instead of returning a result records nothing, that an identifier written to a log is data an account erasure cannot reach, that row-level security must be switched on before a policy is written on top of it, and the dual-storage timezone rules the mirror relies on; the rate limiter added to request_join_group, whose shape the preview RPC copies.

Built (8 September 2026): Step A — §1A, §§3–6 and §10 as they concern RSVP invitations; the database migration that added event invitations, and __tests__/event_invitations_rls.test.ts. Built (8 September 2026): Step B — §1B and §7; the database migration that added event groups, __tests__/event_groups_rls.test.ts, lib/__tests__/eventGroups.test.ts, __tests__/app/groups-event-section.test.tsx, agent-server/checks/eventGroups.check.ts. Built (9 September 2026): §8 — the event-group photo wall, with the photo-encryption work.