feat(event)!: open the calendar, keep editing with its owners #205

Merged
nalum merged 1 commit from feat/household-visible-calendar into main 2026-08-26 18:33:11 +00:00
Owner

Closes #188. Implements the decision in #187, and closes the escalation
that decision opened.

The disagreement. ListOccurrences has always returned every event in
the window to any authenticated caller, guests included, while Read
refused anyone off the member list. The grid drew your partner's dentist
appointment and then denied the tap that opened it. AddUser required
membership too, so nobody but an admin could put themselves on the school
run they could plainly see.

The opening. Everyone reads the whole calendar. Everyone the matrix
admits — all but guests — may join any event on it. Read drops its
membership guard, and AddUser takes a new svc.RequireEventJoiner seam
that passes a self-add.

What opening it broke, and how this closes it. Every edit guard asked
whether a caller appeared on Event.users. That was a fair proxy while
only a member could get on the list, and meaningless the moment anyone
could add themselves — joining an event would have carried the right to
rewrite or delete it.

So attendance and authority are now separate. A self-add records
RELATION_TYPE_ATTENDEE, a new enum value with nothing renamed or
renumbered (ADR-0035); adding someone else still records an owner.
Presence still answers who is going, who hears a reminder and what
ListEvents returns. Authority asks IsEventOwner.

RequireEventLeaver mirrors the joiner so nobody is trapped on an event
they joined, and Update's guest-list rewrite now keeps each person's
existing relation — without that, the next save of any edit would have
promoted every attendee back to owner and eroded the split within a day.
RequireEventMember is deleted rather than left unused: a presence-based
guard in the authz package is a trap now that presence is self-service.

Nobody currently on an event changes. Every stored row was written
through OwnerRef, and an integration spec writes a row straight into the
table as protojson, bypassing the handlers, to prove its owner keeps every
power.

ADR-0036 records the reasoning; ADR-0009 point 2 is amended in place.

Still outstanding, and recorded in the ADR: the web and Android clients
gate editing on the role matrix alone, with no membership term at all, so
they over-show controls to a non-member today and will do the same for an
attendee. That is #193's work.

🤖 Generated with Claude Code

Closes #188. Implements the decision in #187, and closes the escalation that decision opened. **The disagreement.** `ListOccurrences` has always returned every event in the window to any authenticated caller, guests included, while `Read` refused anyone off the member list. The grid drew your partner's dentist appointment and then denied the tap that opened it. `AddUser` required membership too, so nobody but an admin could put themselves on the school run they could plainly see. **The opening.** Everyone reads the whole calendar. Everyone the matrix admits — all but guests — may join any event on it. `Read` drops its membership guard, and `AddUser` takes a new `svc.RequireEventJoiner` seam that passes a self-add. **What opening it broke, and how this closes it.** Every edit guard asked whether a caller appeared on `Event.users`. That was a fair proxy while only a member could get on the list, and meaningless the moment anyone could add themselves — joining an event would have carried the right to rewrite or delete it. So attendance and authority are now separate. A self-add records `RELATION_TYPE_ATTENDEE`, a new enum value with nothing renamed or renumbered (ADR-0035); adding someone else still records an owner. Presence still answers who is going, who hears a reminder and what `ListEvents` returns. Authority asks `IsEventOwner`. `RequireEventLeaver` mirrors the joiner so nobody is trapped on an event they joined, and `Update`'s guest-list rewrite now keeps each person's existing relation — without that, the next save of any edit would have promoted every attendee back to owner and eroded the split within a day. `RequireEventMember` is deleted rather than left unused: a presence-based guard in the authz package is a trap now that presence is self-service. **Nobody currently on an event changes.** Every stored row was written through `OwnerRef`, and an integration spec writes a row straight into the table as protojson, bypassing the handlers, to prove its owner keeps every power. ADR-0036 records the reasoning; ADR-0009 point 2 is amended in place. Still outstanding, and recorded in the ADR: the web and Android clients gate editing on the role matrix alone, with no membership term at all, so they over-show controls to a non-member today and will do the same for an attendee. That is #193's work. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(event)!: open the calendar, keep editing with its owners
All checks were successful
check / commits (pull_request) Successful in 8s
check / go (pull_request) Successful in 2m39s
android / build (pull_request) Successful in 6m15s
check / web (pull_request) Successful in 4m3s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 3s
86cbe67051
Two doors onto the same events disagreed. ListOccurrences has always
returned every event in the window to any authenticated caller, guests
included, while Read refused anyone off the event's member list — so the
grid drew your partner's dentist appointment and then denied the tap that
opened it. AddUser required membership too, which meant nobody but an
admin could put themselves on the school run they could plainly see.

Issue #187 settled it the way a shared family calendar needs: everyone
reads the whole calendar, and everyone the matrix admits (all but guests)
may join any event on it. Read drops its membership guard, and AddUser
takes the new svc.RequireEventJoiner seam, which passes a self-add.

Opening the door made the old boundary wrong. Every edit guard asked
whether a caller appeared on Event.users, which was a fair proxy while
only a member could get on that list — and became meaningless the moment
anyone could add themselves. Left alone, joining an event would have
carried the right to rewrite or delete it.

So attendance and authority are now separate. A self-add records
RELATION_TYPE_ATTENDEE (a new enum value; nothing renamed or renumbered,
ADR-0035) while adding someone else still records an owner. Presence
still answers who is going, who hears a reminder, and what ListEvents
returns. Authority asks IsEventOwner, so Update, Delete, SplitOccurrence,
CancelOccurrence and removing another person all need an owner relation.
RequireEventLeaver mirrors the joiner so nobody is trapped on an event
they joined, and Update's guest-list rewrite keeps each person's existing
relation rather than promoting every attendee back to owner on the next
save.

Every stored row was written through OwnerRef, so no existing household
changes: what they could do yesterday they can do today.

ADR-0036 records the reasoning and amends ADR-0009 point 2.

BREAKING CHANGE: EventService.Read no longer returns PermissionDenied to
a caller who is not a member of the event, and EventService.AddUser now
accepts a self-add from a non-member. A caller who joined an event may
read, leave and attend it, but may not edit or delete it — clients that
inferred edit rights from membership must ask for the owner relation.

Test report

Suite Tests Result Skipped
Unit 1410 ✅ pass 1
Integration 113 ✅ pass —

Coverage: 26.9%

Updated by the check workflow · commit 75fdb5dc9f

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1410 | ✅ pass | 1 | | Integration | 113 | ✅ pass | — | **Coverage:** 26.9% <sub>Updated by the check workflow · commit 75fdb5dc9f0a77242ecac40a8650e5a14103f6b9</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 43 ✅ pass 0

Coverage: 3.0% of lines

Updated by the android workflow · commit 75fdb5dc9f

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 43 | ✅ pass | 0 | **Coverage:** 3.0% of lines <sub>Updated by the android workflow · commit 75fdb5dc9f0a77242ecac40a8650e5a14103f6b9</sub>
nalum force-pushed feat/household-visible-calendar from 86cbe67051
All checks were successful
check / commits (pull_request) Successful in 8s
check / go (pull_request) Successful in 2m39s
android / build (pull_request) Successful in 6m15s
check / web (pull_request) Successful in 4m3s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 3s
to 75fdb5dc9f
Some checks failed
check / commits (pull_request) Successful in 15s
check / go (pull_request) Successful in 2m52s
check / web (pull_request) Successful in 4m30s
android / build (pull_request) Successful in 6m56s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
android / build (push) Has been cancelled
android / report (push) Has been cancelled
check / commits (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
2026-08-26 07:03:33 +00:00
Compare
nalum changed target branch from fix/step-deadline-via-update to main 2026-08-26 18:33:07 +00:00
nalum merged commit 75fdb5dc9f into main 2026-08-26 18:33:11 +00:00
nalum deleted branch feat/household-visible-calendar 2026-08-26 18:33:12 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!205
No description provided.