---
module: devices.device-groups
title: Device Groups
tab: Claude
order: 40
audience: dev
---

## Owner
- Service: **hiveops-devices** (port 8096, DB `hiveiq_devices`). Groups live entirely here.
- Controller `AtmGroupController`, base `@RequestMapping({"/api/atm-groups","/api/device-groups"})` (Phase-2 alias — both route).
- External via NGINX: `https://api.bcos.cloud/devices/api/atm-groups`.

## Endpoints (METHOD path — auth)
| Method | Path | Auth | Body / returns |
|--------|------|------|----------------|
| GET | `/api/atm-groups` | any authed | list, institution-scoped |
| GET | `/api/atm-groups/{id}` | any authed | one group |
| POST | `/api/atm-groups` | admin | `{name, description?, color?, institutionKey?, atmIds?[]}` |
| PUT | `/api/atm-groups/{id}` | admin | partial update; only non-null fields applied |
| DELETE | `/api/atm-groups/{id}` | admin | 204 |
| PUT | `/api/atm-groups/{id}/atms` | admin | body = `[atmId,...]` → **replaces** all members |
| POST | `/api/atm-groups/{id}/atms/{atmId}` | admin | add one |
| DELETE | `/api/atm-groups/{id}/atms/{atmId}` | admin | remove one |
| POST | `/api/atm-groups/{id}/hardware-profile` | admin | `{"profileKey":"..."}` → `{"atmIds":[...]}` |
| POST | `/fleet/tasks` | authed | fleet svc; frontend queues `CONFIG_UPDATE` after profile assign |

- admin = `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`.
- GET endpoints have **no** `@PreAuthorize` (only institution scoping).

## Tables
- `atm_groups` — id, name (**UNIQUE, global**), description, `color`, `institution_key` (len 10), createdAt, updatedAt.
- `atm_group_members` — join (`group_id`, `atm_id`); `@ManyToMany AtmGroup↔Atm`.
- `atms` — written only by `/hardware-profile` (sets `atm.hardware_profile`, lowercased+trimmed).

## Kafka
- Topic: `hiveops.device.events`, type `DEVICE_UPDATED`, key = atmId.
- Emitted **only** by `/hardware-profile` (one event per member, via `DeviceEventPublisher.publishUpdated`).
- Group create/update/delete/membership changes emit **nothing**.

## Gotchas
- **Default group is client-side only** — browser `localStorage` key `hiveops_defaultGroupId`. Not in DB, not per-account. Drives `groupId` filter on `/api/atms/paginated`.
- **Group name UNIQUE is global**, not per-institution → duplicate name = DB constraint error; `server.error.include-message=never` hides detail.
- **`PUT .../atms` replaces the whole set** — not additive. Must send full membership.
- Single-institution caller: create **forces** `institution_key = scope` regardless of request `institutionKey`.
- `verifyGroupAccess` only blocks when `institution_key != null`; **null ("All") groups are editable/deletable by any in-scope admin**.
- Profile assign = 2 effects: DB+Kafka from backend, `CONFIG_UPDATE` fleet task from **frontend** (skipped if `atmIds` empty). Calling `/hardware-profile` directly does NOT queue the fleet task.
- `configPatch` key used by the push = `atm.hardware.profile`.

## Do NOT
- Do NOT expect `Set Default` to persist server-side or across browsers.
- Do NOT assume group changes propagate via Kafka / device_summary mirrors — they don't; only per-ATM hardware-profile updates do.
- Do NOT treat `PUT .../atms` as add — it overwrites membership.
- Do NOT invent a group Kafka topic or a `hiveiq_*` table other than `atm_groups` / `atm_group_members`.
- Do NOT queue a fleet task by calling only the backend `/hardware-profile`; the fleet push is a separate frontend call.
- Do NOT rely on error bodies — messages are suppressed in prod.
