---
module: msp.institutions
title: Institutions
tab: Internal
order: 20
audience: internal
---

Ops/support view of the MSP **Institutions** screen (`msp.bcos.cloud`, frontend `hiveops-msp`, port 5186, image `hiveiq-msp-frontend`). The SPA has **no backend of its own** — every action hits **hiveops-mgmt** (institution list + contact) or **hiveops-devices** (agent tokens) through the NGINX API gateway.

## Who can use it
- Every endpoint behind this screen is gated by `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`.
- **MSP_ADMIN** — sees only institutions granted to *their own MSP*. Access is derived **DB-fresh from the user's email domain** (`customers.domain`), NOT from the JWT institution-key claim (that claim goes stale when new institutions are added after last login).
- **BCOS_ADMIN** — sees **all** institutions (`findAll()`), plus every MSP company group appended at the end.

## What each column is really reading
| Column | Source (hiveops-mgmt / `hiveiq_mgmt` DB) |
|--------|------------------------------------------|
| Institution name + key | `customer_institution_access.institution_name` / `institution_key` |
| Granted | `customer_institution_access.granted_at` |
| Contact (name/email/phone) | `customers.name` / `email` / `phone` (joined by institution key) |
| License key + expiry | active `licenses` row via `findActiveByInstitutionKeyIn` (`license_key`, `expires_at`) |

Agent tokens come from a **different service + DB**: hiveops-devices, table `agent_tokens` in `hiveiq_devices`.

## First things to check when it misbehaves

**"MSP_ADMIN sees no institutions / empty list"**
- The list is keyed off email domain. Confirm a `customers` row exists with `customer_type = MSP` and `domain` = the exact domain of the logged-in user's email. No matching domain → the endpoint returns `[]` (HTTP 200, empty), not an error.
- Confirm `customer_institution_access` rows exist for that MSP customer id.

**"An institution is missing but the MSP should manage it"**
- Missing row in `customer_institution_access` for that `customer_id` + `institution_key`. Grant it there.

**"License shows dash / No expiry unexpectedly"**
- No active `licenses` row for that institution key, or `expires_at` is null (renders as *No expiry*). Expiry turns amber ≤30 days, red once past.

**"Edit Contact fails with a conflict"**
- `PUT .../contact` returns **409** if the new email already exists on another customer (`existsByEmail`). Use a unique email.
- Returns **403** if an MSP_ADMIN edits an institution their MSP was never granted (`existsByCustomerIdAndInstitutionKey` check).
- Returns **404** if no `customers` row matches the institution key.

**"Generated agent token doesn't authenticate the ATM"**
- KNOWN SPLIT: the MSP UI generates tokens into hiveops-**devices** (`hiveiq_devices.agent_tokens`), but the agent-proxy validates bearer tokens against hiveops-**mgmt** (`services.mgmt.url` → `/api/internal/agent-tokens/validate`, `hiveiq_mgmt.agent_tokens`). A token minted here may not be recognised at agent auth until the two stores are reconciled. Verify which table the validating path actually reads before declaring a token bad. (See Architect tab; tracked as an open token-data-migration item.)

**"Revoked token still works for ~5 min"**
- agent-proxy caches validation results (`proxy.agent-token.cache-seconds`, default 300). Expected; wait out the cache.

**"List looks stale"**
- Auto-refresh is client-side, 60s, toggle persisted in `localStorage` key `msp_institutions_autoRefresh`. Hit the manual refresh (↻) button.

## Admin-only actions on this screen
- **Edit Contact** — writes `customers.name/email/phone`.
- **Tokens** — generate / revoke / delete-revoked agent tokens (raw value shown **once** at creation; only the SHA-256 hash + 8-char prefix are stored).
- **Readiness** — read-only pre-flight; **computed entirely in the browser** (no backend endpoint) from institution fields + the token list.

> Token revocation reminder: before revoking an agent token that ATMs are actively using, push a fleet-wide CONFIG_UPDATE to clear it first — revoking a live token drops the agents' module threads.
