---
module: msp.institution-keys
title: Institution Keys
tab: Architect
order: 30
audience: dev
---

> How it's built. Grounded in `hiveops-devices` + `hiveops-msp` code, 2026-07-01. Cross-links: [platform.data-architecture], [platform.kafka], [platform.service-ownership].

## Ownership
Despite living in the **MSP** SPA (`msp.bcos.cloud`), this feature is owned end-to-end by **hiveops-devices**. The MSP frontend has no backend of its own; `hiveops-msp/frontend/src/lib/api.ts` points `api` at `window.__APP_CONFIG__.apiUrl` and hits `/devices/institution-keys`, which NGINX routes to the devices backend at `/api/institution-keys`. This respects [platform.service-ownership] — devices is the single source of truth for all device-domain data.

Components:
- **Frontend:** `hiveops-msp/frontend/src/components/InstitutionKeys/InstitutionKeysView.svelte` — table + slide-in add/edit panel + delete-confirm modal, 60s auto-refresh. (An equivalent `Settings/InstitutionKeys` view also ships inside the Devices app.)
- **Controller:** `com.hiveops.devices.controller.InstitutionKeyController` (`@RequestMapping("/api/institution-keys")`).
- **Service:** `com.hiveops.devices.service.InstitutionKeyService` (Spring cache-backed).
- **Entity/Repo:** `entity.InstitutionKey` / `repository.InstitutionKeyRepository` (Spring Data JPA, `@Id` = `key`).

## Data model (hiveiq_devices)
`institution_keys` — created in migration `V2__create_atm_core_tables.sql`:

| Column | Type | Notes |
|--------|------|-------|
| `key` | VARCHAR(10) | PRIMARY KEY, stored upper-cased/trimmed |
| `institution_name` | VARCHAR(255) | NOT NULL, **UNIQUE** |
| `supported_device_types` | VARCHAR(20) | default `'ATM,TCR'`; CSV of `DeviceType` (`ATM,TCR,SRV`) |

Related: `institution_agent_properties` (`V3__create_atm_properties.sql`) — PK `institution_key` **REFERENCES institution_keys(key) ON DELETE CASCADE**; holds `hiveops_config` / `ej_config` JSON + `config_pushed_at`. `institution_config_audit` records every agent-properties change (`changed_by`, `previous_config`, `new_config`).

## Read/write flow
**CRUD (admin):**
`InstitutionKeysView` → `institutionKeyAPI.{getAll|create|update|delete}` → NGINX `/devices/*` → devices `InstitutionKeyController` → `InstitutionKeyService` → `InstitutionKeyRepository` → `institution_keys`.
- `getAll()` delegates to `getAccessible()`: `InstitutionContext.resolveScope()` returns the JWT institution-key list (CUSTOMER = 1, MSP_ADMIN = many) or `null` (admins, unrestricted). Scoped callers get `findByKey` per granted key; unrestricted callers get `repository.findAll()`.
- `create/update/delete` are `@CacheEvict(value="institutionKeys", key="#key")`; `findByKey` is `@Cacheable`. `getAll` is uncached.

**Registration gate (the key's real job):**
Agent → **agent-proxy** → devices `InternalAtmController.register` (`POST /api/internal/atms/register?deviceType=&institutionKey=`):
1. `institutionKeyService.findByKey(institutionKey)`.
2. If mapped and `supported_device_types` non-blank and does not contain the parsed `DeviceType` → **403 `{linked:false,error}`** (registration rejected).
3. Else `findOrCreateAtm`, and if `atms.institution` is blank, set it to the mapping's `institution_name`.
4. `DeviceEventPublisher.publishCreated(atm)` emits to Kafka.

**Scope translation (every device query):**
`AtmService` / `AtmModuleService` call `InstitutionKeyService.resolveInstitutionNames(keys)` to turn JWT **keys** into `atms.institution` **names** before filtering. This is the join that makes key→name scoping work platform-wide.

## Kafka
See [platform.kafka]. Note the important asymmetry:
- **Institution-key CRUD publishes NOTHING.** `InstitutionKeyService` has no producer. Renaming/adding/deleting a key does not emit an event — no `device_summary` mirror update, no reconciler.
- Device **registration/changes** publish `hiveops.device.events` via `DeviceEventPublisher`; that topic is what mirrors device (incl. institution name) into incident/fleet/reports/recon/vault per [platform.data-architecture]. So a key rename only propagates once affected devices re-publish through their own lifecycle.

## Key API endpoints (external path via `/devices` → internal `/api`)
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/institution-keys` (+ `/accessible`) | any JWT, scoped | list mappings |
| POST | `/api/institution-keys` | MSP_ADMIN/BCOS_ADMIN | create |
| PUT | `/api/institution-keys/{key}` | MSP_ADMIN/BCOS_ADMIN | rename / change device types |
| DELETE | `/api/institution-keys/{key}` | MSP_ADMIN/BCOS_ADMIN | delete (cascades agent props) |
| GET/PUT | `/api/institution-keys/{key}/agent-properties` | read: scoped / write: MSP_ADMIN/BCOS_ADMIN | per-institution agent config |
| GET | `/api/institution-keys/{key}/agent-properties/flat` | scoped | flat dot-notation config (CONFIG_UPDATE payload) |
| GET | `/api/institution-keys/config-status` / `/config-audit` | scoped | config push status / audit log |
| POST | `/api/internal/atms/register` | agent-proxy internal | consumer of the device-type policy |

## Notes for maintainers
- `create()` uses `repository.save()` on an `@Id` entity → posting an **existing key overwrites** it (JPA merge), no 409.
- No Kafka on key changes means downstream name mirrors can drift after a rename; treat renames as a data-migration event, not a config toggle.
