---
module: msp.institutions
title: Institutions
tab: Claude
order: 40
audience: dev
---

Act-without-guessing reference. Frontend = `hiveops-msp/frontend` `Institutions/InstitutionsView.svelte` (port 5186, `msp.bcos.cloud`, image `hiveiq-msp-frontend`, no own backend).

## Ownership
- Institution list + contact → **hiveops-mgmt** (`MspController`, `@RequestMapping("/api/v1/msp")`).
- Agent tokens → **hiveops-devices** (`AgentTokenController`, `@RequestMapping("/api/agent-tokens")`).
- Gateway prefixes: `/mgmt/*`→mgmt, `/devices/*`→devices.

## Endpoints (METHOD + external path)
| METHOD | Path | Backend | Roles |
|--------|------|---------|-------|
| GET | `/mgmt/api/v1/msp/institutions` | mgmt `MspController#getInstitutions` | MSP_ADMIN, BCOS_ADMIN |
| PUT | `/mgmt/api/v1/msp/institutions/{institutionKey}/contact` | mgmt `#updateInstitutionContact` | MSP_ADMIN, BCOS_ADMIN |
| GET | `/devices/agent-tokens?institutionKey=<key>` | devices `AgentTokenController#list` | MSP_ADMIN, BCOS_ADMIN |
| POST | `/devices/agent-tokens` (body `{institutionKey,name,customerId}`) | devices `#generate` → 201 | MSP_ADMIN, BCOS_ADMIN |
| POST | `/devices/agent-tokens/{id}/revoke` | devices `#revoke` | MSP_ADMIN, BCOS_ADMIN |
| DELETE | `/devices/agent-tokens/revoked?institutionKey=<key>` | devices `#deleteRevoked` | MSP_ADMIN, BCOS_ADMIN |
| POST | `/api/internal/agent-tokens/validate` (agent-proxy→mgmt, internal) | mgmt `AgentTokenInternalController#validate` | internal secret |

Readiness panel = **client-side only** (`computeChecks()`); no endpoint.

## Roles
- All UI endpoints: `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`.
- MSP_ADMIN scope derived from `customers.domain` = email domain (NOT the JWT institution-key claim). BCOS_ADMIN = all institutions.

## Tables
| DB | Table | Cols of note |
|----|-------|--------------|
| `hiveiq_mgmt` | `customer_institution_access` | institution_key, institution_name, granted_at, customer_id |
| `hiveiq_mgmt` | `customers` | name, email, phone, domain, institution_key, company, customer_type (BANK/MSP) |
| `hiveiq_mgmt` | `licenses` | license_key, expires_at |
| `hiveiq_mgmt` | `agent_tokens` | validation master (agent-proxy reads here) |
| `hiveiq_devices` | `agent_tokens` | token_prefix, token_hash(SHA-256), institution_key, customer_id, status, last_used_at — **MSP UI writes here** |

## Topics
- None in this feature's read/write path. (`hiveops.device.events` exists in devices but is not emitted by token/contact mutations.)

## Token format
- `agtkn_<prefix8>_<secret32>`; only SHA-256 hash + 8-char prefix stored; raw shown once at generation.

## Gotchas
- **Split token store**: UI generates into `hiveiq_devices.agent_tokens`; agent-proxy validates against `hiveiq_mgmt.agent_tokens` (`services.mgmt.url` + `/api/internal/agent-tokens/validate`). Tokens minted in the MSP UI may not authenticate until stores are reconciled (open migration item).
- List response is a raw `Map`, not a typed DTO — field names come straight from `MspController` (`licenseKey`, `licenseExpiresAt`, `contactName/Email/Phone`, `customerId`, `type`).
- MSP_ADMIN with no matching `customers` row for its email domain → `[]` + HTTP 200 (looks like "no data", not an error).
- Contact PUT: 409 = email already used (`existsByEmail`); 403 = MSP lacks access to key; 404 = no customer for key.
- Auto-refresh 60s, client-side; `localStorage` key `msp_institutions_autoRefresh`.
- Revoked-token grace: agent-proxy caches validation `proxy.agent-token.cache-seconds` (default 300s).

## Do NOT
- Do NOT assume the JWT institution-key claim scopes MSP_ADMIN — scope is DB-derived from email domain.
- Do NOT expect a readiness API — it is computed in the browser.
- Do NOT expect Kafka events from contact edits or token CRUD.
- Do NOT trust that a UI-generated token is agent-valid without checking the mgmt validation table.
- Do NOT revoke a live agent token before pushing a fleet-wide CONFIG_UPDATE to clear it (kills agent module threads).
- Do NOT look for a `hiveops-msp` backend — there isn't one; it calls mgmt + devices.
