---
module: msp.institutions
title: Institutions
tab: Architect
order: 30
audience: dev
---

How the MSP **Institutions** feature is built. It is a thin Svelte SPA that composes data from **two independently-owned backends** — no aggregation layer, no message bus in the read/write path.

## Components
- **Frontend** — `hiveops-msp/frontend`, `components/Institutions/InstitutionsView.svelte`. Svelte 4 + TS, port 5186, domain `msp.bcos.cloud`, image `hiveiq-msp-frontend`. Runtime API base injected as `window.__APP_CONFIG__.apiUrl`; JWT taken from `localStorage.token` and sent as `Authorization: Bearer`.
- **hiveops-mgmt** — owns institution membership, contact details, licensing. Serves the list + contact edit.
- **hiveops-devices** — owns agent tokens. Serves list/generate/revoke/delete-revoked.
- **NGINX gateway** — path prefix routing: `/mgmt/*` → hiveops-mgmt, `/devices/*` → hiveops-devices (`/devices/` rewrites onto the devices `/api/` base). See [platform.service-ownership].

## Data flow

**Institution table (read)**
```
InstitutionsView.load()
  → GET /mgmt/api/v1/msp/institutions
  → hiveops-mgmt MspController#getInstitutions   @RequestMapping("/api/v1/msp")
     ├─ BCOS_ADMIN → CustomerInstitutionAccessRepository.findAll()
     └─ MSP_ADMIN  → resolve MSP customer by email domain (customers.domain)
                     → findByCustomerIdOrderByInstitutionNameAsc(customerId)
     ├─ LicenseRepository.findActiveByInstitutionKeyIn(keys)   (license_key, expires_at)
     ├─ CustomerRepository.findByInstitutionKeyIn(keys)        (name/email/phone/domain)
     └─ appends MSP company group row(s) for admin's own users
```
Response is a hand-built `List<Map<String,Object>>` (not a DTO): `institutionKey, name, grantedAt, licenseKey, licenseExpiresAt, type, customerId, domain, contactName, contactEmail, contactPhone`.

**Contact edit (write)**
```
saveContact()
  → PUT /mgmt/api/v1/msp/institutions/{institutionKey}/contact  {name,email,phone}
  → MspController#updateInstitutionContact
     ├─ MSP_ADMIN access guard: existsByCustomerIdAndInstitutionKey(...)  → 403
     ├─ email uniqueness: existsByEmail(...)                              → 409
     ├─ customer lookup: findByInstitutionKey(...)                        → 404 if absent
     └─ customers.save(name,email,phone)
```

**Agent tokens (write/read, different service)**
```
agentTokenAPI.*  → /devices/agent-tokens...
  → hiveops-devices AgentTokenController   @RequestMapping("/api/agent-tokens")
  → AgentTokenService → agent_tokens (hiveiq_devices)
     generate: raw = "agtkn_" + prefix(8) + "_" + secret(32); store SHA-256(raw) as token_hash,
               prefix, institution_key, customer_id, status=ACTIVE. Raw returned once.
```

**Readiness** — no backend call. `computeChecks()` runs in the browser over the institution row + the token list (`agentTokenAPI.list`), grouping checks into Customer Profile / Licensing / Agent Connectivity.

## Kafka
- **None in this feature's path.** Institution list, contact edit, and token CRUD are all synchronous REST + JPA. hiveops-devices publishes `hiveops.device.events` for *device* data generally, but agent-token and institution-contact mutations here do **not** emit events. See [platform.kafka].

## DB tables
| Service / DB | Table | Read/Write | Columns used |
|--------------|-------|-----------|--------------|
| mgmt / `hiveiq_mgmt` | `customer_institution_access` | R | institution_key, institution_name, granted_at, customer_id |
| mgmt / `hiveiq_mgmt` | `customers` | R/W | name, email, phone, domain, institution_key, company, customer_type |
| mgmt / `hiveiq_mgmt` | `licenses` | R | license_key, expires_at (active only) |
| devices / `hiveiq_devices` | `agent_tokens` | R/W | token_prefix, token_hash, institution_key, customer_id, status, last_used_at |

See [platform.data-architecture].

## Key endpoints
| Method | External path (via gateway) | Service / controller | Roles |
|--------|------------------------------|----------------------|-------|
| GET | `/mgmt/api/v1/msp/institutions` | mgmt `MspController#getInstitutions` | MSP_ADMIN, BCOS_ADMIN |
| PUT | `/mgmt/api/v1/msp/institutions/{key}/contact` | mgmt `MspController#updateInstitutionContact` | MSP_ADMIN, BCOS_ADMIN |
| GET | `/devices/agent-tokens?institutionKey=` | devices `AgentTokenController#list` | MSP_ADMIN, BCOS_ADMIN |
| POST | `/devices/agent-tokens` | devices `AgentTokenController#generate` | MSP_ADMIN, BCOS_ADMIN |
| POST | `/devices/agent-tokens/{id}/revoke` | devices `AgentTokenController#revoke` | MSP_ADMIN, BCOS_ADMIN |
| DELETE | `/devices/agent-tokens/revoked?institutionKey=` | devices `AgentTokenController#deleteRevoked` | MSP_ADMIN, BCOS_ADMIN |

## Cross-service note (token master)
Two `agent_tokens` tables exist: this UI writes to hiveops-**devices** (`hiveiq_devices`), while the runtime agent-proxy validates tokens against hiveops-**mgmt** (`AgentTokenValidationService` → `services.mgmt.url` + `/api/internal/agent-tokens/validate`, `hiveiq_mgmt.agent_tokens`). Reconciling these two stores is an open item — treat the mgmt table as the auth master until then. See [platform.service-ownership].
