---
module: msp.contact-groups
title: Contact Groups
tab: Internal
order: 20
audience: internal
---

Ops/support view for the MSP **Contact Groups** screen. This is a distribution-list feature: an admin builds groups of HiveIQ users, subscribes each group to notification event types, and can fan a system notification out to every subscribed group as an in-app ACTION message.

## Where it actually lives

- **Frontend:** `hiveops-msp` SPA — `ContactGroupsView.svelte` (`msp.bcos.cloud`, port 5186). No MSP backend of its own.
- **Backend / data owner:** **`hiveops-messaging`** (port 8086, DB `hiveiq_messaging`). All group data and the notify action are served here.
- **User list** for the member picker comes from **`hiveops-auth`** — `GET /auth/api/msp/users?size=500`. Members are stored as auth user IDs (`BIGINT`), not emails.

## Roles required

- **Read** (list groups, open a group, view subscriptions): any authenticated JWT.
- **All mutations** (create, update, delete, add/remove member, add/remove subscription, **send notification**): admin only. Backend gate is `MessagingPrincipal.isAdmin()`, which accepts role `ADMIN`, `MSP_ADMIN`, or `BCOS_ADMIN` (case-insensitive). Role is read from the JWT `role` claim.
- There is **no `@PreAuthorize`** — the check is in `ContactGroupService.requireAdmin()` and, for notify, directly in `ContactGroupController.notify()`.

## Admin actions on this screen

- **+ New Group** — name (required, unique, ≤100 chars) + optional description (≤500 chars).
- **Manage** (row / panel) — edit name/description, toggle subscriptions, add/remove members.
- **Subscriptions** — the four valid types are fixed: `SYSTEM_UPDATE`, `INCIDENT_SUMMARY`, `REPORT_SYSTEM_CHANGE`, `SYSTEM_ANNOUNCEMENT`.
- **Send Notification** — fans an ACTION message out to every member of every group subscribed to the chosen type. Response reports `dispatched` (recipient count). Delivered messages land in each user's normal messaging inbox (the Electron app polls unread count).

## First things to check when it misbehaves

1. **Whole screen fails to load / "Failed to load groups"** — `hiveops-messaging` is down or the JWT is bad. `curl http://localhost:8086/actuator/health` on the services VM; check the token has a `role` claim.
2. **CORS / network errors in the browser** — Spring CORS is **disabled** in messaging (`.cors(disable)`); **nginx is the CORS authority**. Confirm `msp.bcos.cloud` is allowed on the `/messaging/` location. This is the usual cause of a working curl but a failing browser call.
3. **"Send Notification" reports `dispatched: 0`** — expected if no group is subscribed to that type, or a subscribed group has zero members. Check the group's Subscriptions and Members before assuming a bug.
4. **403 on any create/edit/delete/notify** — the caller's JWT role is not one of `ADMIN` / `MSP_ADMIN` / `BCOS_ADMIN`. Read succeeds but writes 403 → it's a role problem, not a routing problem.
5. **"A group named 'X' already exists"** — group `name` is UNIQUE at the DB level; create and rename both reject duplicates.
6. **Member shows as "User #123"** instead of a name — the auth user list (`/auth/api/msp/users`) didn't load or the user isn't in the returned 500-row page. Group membership itself is fine; it's a display-only lookup miss.
7. **Recipient didn't see the notification** — it's persisted as an ACTION message in the `messages` table; check the recipient's inbox, not a Kafka topic. The notify path does **not** publish to Kafka.

## Common failure modes + fixes

| Symptom | Likely cause | Fix |
|--------|--------------|-----|
| All groups load but writes 403 | Non-admin role in JWT | Re-issue token with `MSP_ADMIN`/`ADMIN`/`BCOS_ADMIN` |
| Browser blocked, curl works | nginx CORS missing `msp.bcos.cloud` | Add origin to messaging location in nginx |
| `dispatched: 0` | No subscribed group, or empty group | Verify subscriptions + membership |
| Notify dispatches nothing for a valid-looking type | Type case/typo mismatch (see gotcha) | Send exact uppercase constant |
| Delete blocked / stale members | — | Members + subscriptions cascade-delete with the group |

## Data footprint (for support queries)

- `contact_groups` — one row per group (`uuid`, `name`, `description`, `created_by_user_id`).
- `contact_group_members` — PK `(group_id, user_id)`; `ON DELETE CASCADE`.
- `group_subscriptions` — `(group_id, event_type)` UNIQUE; `ON DELETE CASCADE`.
- Delivered notifications → `messages` rows (`type = ACTION`, `sender_email = 'system'`).

> **Scope note:** groups are **not** institution-scoped — there is no `institution_key` on `contact_groups`, and `getAllGroups()` returns every group. Any authenticated user can read every group across all institutions. Treat "why can user X see group Y" as expected current behavior, not a bug.
