# Groups - Technical Architecture **Feature:** Groups Management **Status:** Implemented (authorization: see [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)) This document records the durable design of the Groups feature: data model, key decisions, integration points, accessibility rules, and the planned extension paths. The original implementation plan (estimations, vertical slices, per-issue acceptance criteria, testing/migration strategy) has been removed now that the feature has shipped. **Related:** [database-schema-readme.md](./database-schema-readme.md), [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md). --- ## Core Design Decisions 1. **Many-to-many:** members can belong to multiple groups and vice versa, via the `member_groups` join table (a separate Ash resource). 2. **Flat structure:** no hierarchy in the current schema; the design leaves a clear path to add it later (see [Future Extensibility](#future-extensibility)). 3. **Minimal attributes:** `name`, `description`, `slug`. The `slug` is auto-generated from `name`, immutable, URL-friendly. 4. **Cascade on the join table only:** deleting a group (or member) removes the `member_groups` associations but never deletes members/groups themselves. Group deletion requires explicit confirmation (typing the group name). 5. **Search integration:** group names are included in the member `search_vector` (not a separate search index). ## Domain & Resources Groups live in the **`Mv.Membership`** domain alongside Members and CustomFields. - `Mv.Membership.Group` (`lib/membership/group.ex`) — attributes `name`, `slug`, `description`; `has_many :member_groups`, `many_to_many :members`; `member_count` aggregate (`count :member_count, :member_groups`); `unique_slug` identity for slug lookups. Slug is generated by the shared **`Mv.Membership.Changes.GenerateSlug`** change (the same change CustomFields uses), generated on create and immutable on update. - `Mv.Membership.MemberGroup` (`lib/membership/member_group.ex`) — join table; `belongs_to :member`, `belongs_to :group`; unique on `(member_id, group_id)`. Has `create`/`destroy` actions only (no `update`); group membership is managed by creating and destroying these join rows. - `Mv.Membership.Member` (extended) — `has_many :member_groups`, `many_to_many :groups`. Group membership is managed through the `MemberGroup` join resource, not via dedicated Member actions. ## Data Model ### `groups` - `id` (UUIDv7), `name` (required), `slug` (required, immutable, auto-generated), `description` (optional), timestamps. - Uniqueness: `name` unique case-insensitively (`UNIQUE` on `LOWER(name)`, index `groups_unique_name_lower_index`); `slug` unique case-sensitively (`groups_unique_slug_index`). ### `member_groups` (join table) - `id` (UUIDv7), `member_id`, `group_id`, timestamps. - Unique `(member_id, group_id)` prevents duplicates; indexes on `member_id` and `group_id`. - **CASCADE delete on both foreign keys** — the cascade is intentionally on the join table only. For exact columns/indexes see `database_schema.dbml`. ## Search Integration Group names are part of the member full-text search: - They are aggregated from `member_groups` joined to `groups` and added to `members.search_vector` at **weight B**. - The trigger `update_member_search_vector_on_member_groups_change` runs `update_member_search_vector_from_member_groups()` on **INSERT/UPDATE/DELETE on `member_groups`** and refreshes the affected member's `search_vector`. - Migration `20260217120000_add_group_names_to_member_search_vector.exs` (Issue #375). No Elixir search change is needed — searching a group name finds its members automatically. ## UI Surface (implemented) - **`/groups`** — index table (name, description, member count, actions), sorted by name at the DB level. Create button → `/groups/new`. - **`/groups/:slug`** — detail: group info, member list, inline add-member combobox (search/autocomplete, excludes members already in the group), per-row remove (no confirmation), edit/delete. Add/remove are guarded by `:update` permission both in the UI and server-side in the event handlers. - **`/groups/:slug/edit`** and **`/groups/new`** — separate form pages; slug not editable. Edit does auth in `mount/3` and loads the group once in `handle_params/3`. - **Delete confirmation modal** — warns with member count (pluralized), requires typing the group name to enable delete (`phx-debounce="200"`), stays open on mismatch, authorizes server-side. - **Member overview** — "Groups" column with badges; filter dropdown (persisted in URL query params); sort by group; group names searchable. - **Member detail** — Groups shown as a data field in Personal Data (below Linked User), button-style links to `/groups/:slug`. ## Accessibility - **Do not use `role="status"` on group badges or navigation links.** That role is for live regions (screen-reader announcements), not for static labels or navigation. Use `aria-label` (e.g. "Member of group X") instead. - `role="status"` with `aria-live="polite"` is appropriate only for dynamic announcements (filter changes, member-count updates). - Clickable filter badges (optional enhancement) must be real `