---
module: msp.contact-groups
title: Contact Groups
tab: Claude
order: 40
audience: dev
---

Act-without-guessing reference. All facts confirmed in `hiveops-messaging` + `hiveops-msp` source.

## Ownership

- **Data + logic owner:** `hiveops-messaging` (port 8086, DB `hiveiq_messaging`).
- **UI:** `hiveops-msp` SPA `ContactGroupsView.svelte` (`msp.bcos.cloud`, 5186). No MSP backend.
- **User directory (member picker):** `hiveops-auth` → `GET /auth/api/msp/users?size=500`.

## Endpoints — base `/api/messaging/groups` (`ContactGroupController`)

| Method | Path | Admin? | Body / notes |
|--------|------|--------|--------------|
| GET | `/api/messaging/groups` | no | list `ContactGroupResponse[]` |
| POST | `/api/messaging/groups` | yes | `{name, description?}` |
| GET | `/api/messaging/groups/{uuid}` | no | detail (`memberUserIds`, `subscribedEventTypes`) |
| PUT | `/api/messaging/groups/{uuid}` | yes | `{name?, description?}` |
| DELETE | `/api/messaging/groups/{uuid}` | yes | cascades members+subs |
| POST | `/api/messaging/groups/{uuid}/members` | yes | `{userIds: number[]}` |
| DELETE | `/api/messaging/groups/{uuid}/members/{userId}` | yes | |
| GET | `/api/messaging/groups/{uuid}/subscriptions` | no | `string[]` |
| POST | `/api/messaging/groups/{uuid}/subscriptions` | yes | `{eventType}` → returns `string[]` |
| DELETE | `/api/messaging/groups/{uuid}/subscriptions/{eventType}` | yes | |
| POST | `/api/messaging/groups/notify` | yes | `{eventType, subject, body, metadata?}` → `{eventType, dispatched}` |

- Path param is the group **`uuid`**, never numeric `id`.
- Frontend `messagingApi` base omits `/api` (nginx prepends it) — actual controller path includes `/api`.

## Roles

- Admin gate = `MessagingPrincipal.isAdmin()` → role ∈ `ADMIN`, `MSP_ADMIN`, `BCOS_ADMIN` (case-insensitive), from JWT `role` claim.
- No `@PreAuthorize`; enforced in `ContactGroupService.requireAdmin()` + `notify()` controller check.
- Reads (list/get/subscriptions) require only a valid JWT.

## Event types (fixed — `NotificationEventType.ALL`)

`SYSTEM_UPDATE` · `INCIDENT_SUMMARY` · `REPORT_SYSTEM_CHANGE` · `SYSTEM_ANNOUNCEMENT`

## Tables (`hiveiq_messaging`, Flyway V4/V5)

| Table | Key | Notes |
|-------|-----|-------|
| `contact_groups` | `uuid` UNIQUE, `name` UNIQUE | + `created_by_user_id`, `created_at`, `updated_at` |
| `contact_group_members` | PK `(group_id, user_id)` | FK `ON DELETE CASCADE` |
| `group_subscriptions` | UNIQUE `(group_id, event_type)` | FK `ON DELETE CASCADE`; `event_type` stored UPPERCASE |
| `messages` | — | delivery target: `type=ACTION`, `sender_email='system'` |

## Kafka

- `notify` does **NOT** publish to Kafka — writes `messages` rows directly.
- Group fan-out from inbound Kafka: `MessageService.persistFromEvent()` on topic **`hiveops.messages`** when `recipientGroupId` (a group uuid) is set.
- Topics: `hiveops.messages`, `hiveops.reports.completed`.

## Gotchas

- `dispatched: 0` is normal when no group is subscribed to the type or subscribed groups are empty — not an error.
- `notify` does not validate `eventType` against `NotificationEventType.ALL` and `findByEventType` does **not** uppercase the input; a lowercase/typo `eventType` silently matches nothing → `dispatched: 0`. Send exact uppercase constants.
- `subscriptions` POST/DELETE uppercase the input; store is uppercase.
- Groups are **global** — no `institution_key` column; `getAllGroups()` returns all groups to any authenticated user.
- Member IDs are auth user IDs; names/emails resolved client-side from `/auth/api/msp/users`.
- CORS: messaging disables Spring CORS — nginx is the authority (curl-OK-but-browser-blocked = nginx origin).

## Do NOT

- Do **not** address groups by numeric `id` — use `uuid`.
- Do **not** expect a Kafka event from `notify` — inspect the `messages` table.
- Do **not** assume institution scoping on groups — there is none.
- Do **not** add group endpoints to `hiveops-msp` or any other service — messaging owns this domain.
- Do **not** POST arbitrary `eventType` to `/notify` and read a 400 — invalid types return 200 with `dispatched: 0`.
- Do **not** send lowercase event types.
