docs: HANDOFF pass-56, Phase 4 notification breadth + migration 047

Adds an ARCHITECTURE section "Notification breadth: bookings, classes,
forms" describing each new event type, the triggering action, the
recipient rule, and the dedupe-key scheme. Updates Known Limitations #5
to reflect that booking + class + form notifications are no longer "not
wired yet."

API_REFERENCE: extends the Transactional notifications section with the
shared enqueue helper, every new event type, its trigger and recipient,
and the new members_with_permission RPC used for the forms.manage
fan-out.

DATABASE_SCHEMA + DB_SCHEMA_MAP: adds the migration 047 row and the new
helper function to the helper-functions list.

HANDOFF: pass-56 entry covers the four locked design forks, the four
sub-phase commits, test growth (532 -> 565, +33 cases), state (local,
inert post-deploy until owner provisions Resend + CRON_SECRET), the
known fan-out volume tradeoff, and the remaining backlog.
This commit is contained in:
Moheeb Zara
2026-05-19 15:32:39 -07:00
parent a2ea7675e7
commit 6abac53380
5 changed files with 102 additions and 4 deletions
+1
View File
@@ -727,6 +727,7 @@ migration is mirrored as a section in it. Tables/columns added:
| 044 | `prevent_member_self_role_change` extended to also block a non-privileged member changing their own `payment_status`, `payment_note`, `dues_paid_until`, `last_paid_at`, `last_payment_at`, `stripe_customer_id`, `joined_at` (RLS lets a member UPDATE their own row, so these were self-settable to forge a dues-good signal). Service-client (`auth.uid()` NULL) and privileged-on-another-member writes still bypass. No new table/column |
| 045 | `class_signup_tx(p_session_id,p_space_id,p_member_id)` + `class_cancel_tx(...)` SECURITY DEFINER, `search_path=public`. Each takes `pg_advisory_xact_lock(hashtext(session_id))` then does the capacity-decision+dup-check+insert (signup) or cancel+conditional waitlist-promote (cancel) atomically. Closes the concurrent over-enroll / double-promote races (dynamic capacity can't be a constraint). Mirrors `computeSignupStatus`/`pickPromotion`; the SQL is the runtime authority. No new table/column/policy |
| 046 | `user_has_role_in_space` and `user_has_permission` extended with `status IN ('current','late')` (mirrors `lib/permissions` `PRIVILEGE_STATUSES`). RLS-layer defense-in-depth for D2 (the require_approval / unverified-admin escalation): a non-privilege-eligible member who somehow bypassed the app guards still cannot exercise a role or permission. `get_user_space_ids` and `user_effective_roles` are deliberately NOT gated so unverified members keep their own /me + onboarding reads (the legitimate require_approval flow). No new table/column/policy |
| 047 | `members_with_permission(sid, perm)` SECURITY DEFINER, `search_path=public`. Inverted, set-returning form of `user_has_permission`: returns `member_id` for every member of `sid` who holds `perm` (same current/late status gate as 046, same admin shortcut, same `space_role_permissions` + `user_effective_roles` fallback). Lets the Phase 4 notification fan-outs (e.g. `form_submission_admin`) find permission holders in one query instead of N round-trips. Additive; existing `user_has_permission` callers unchanged. No new table/column/policy |
New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`.