---
module: claims.vendors
title: Vendors
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** How the Vendor Registry feature is built. All facts grounded in hiveops-claims source (2026-07-01). See [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Shape of the feature

Vendors is the simplest slice of hiveops-claims: a single-entity CRUD-lite (create + read only) registry, fully self-contained in the **hiveops-claims** service. There are **no other services, no Kafka, and no cross-service calls** in this feature — it is one Svelte page, one controller, one service, one repository, one table.

```
VendorsPage.svelte / AddVendorModal.svelte
        │  vendorAPI.list() / vendorAPI.create()   (frontend/src/lib/api.ts)
        ▼
GET|POST  api.bcos.cloud/claims/api/claims/vendors   (NGINX → hiveiq-claims:8092)
        ▼
VendorController  ──►  VendorService  ──►  VendorRepository (JPA)  ──►  Postgres: vendors
```

Unlike claims (which do the executor→Kafka→record-store and `device_summary` mirror dance), **vendors never touch Kafka**. hiveops-claims consumes `hiveops.device.events` only to keep its `device_summary` read-model current for claim creation — that path is unrelated to the vendor registry.

## Services involved

- **hiveops-claims** — owns everything for this feature. Backend `hiveiq-claims` (port 8092, `/api/claims/...`), frontend `hiveiq-claims-frontend` (`claims.bcos.cloud`, port 5179).
- No incident / journal / devices / mgmt involvement for vendor read or write.

## Data flow

**Read (`GET /api/claims/vendors`)**
1. `VendorController.listVendors` reads `principal.institutionKey()` (populated by `JwtAuthenticationFilter` from the JWT `institutionKey` claim; principal type `ClaimsPrincipal(userId, email, role, institutionKey)`).
2. Optional `?type=CIT|SP` binds to `Vendor.VendorType`. With a type → `listVendorsByType`; without → `listVendors`.
3. `VendorService` → `VendorRepository.findByInstitutionKeyOrderByNameAsc` (or `...AndTypeOrderByNameAsc`).
4. Rows map to `VendorResponse` (uuid, name, type, contactName/Email/Phone, createdAt).
5. Frontend `VendorsPage` receives the flat list and splits it **client-side** into CIT vs SP (`vendors.filter(v => v.type === 'CIT')`), rendering two tables. (Note: the page's `typeFilter` variable exists but is not bound to any UI control, so the server-side `?type=` filter is effectively unused today.)

**Write (`POST /api/claims/vendors`)**
1. `AddVendorModal` posts `{ name, type, contactName?, contactEmail?, contactPhone? }` (empty optionals omitted).
2. `CreateVendorRequest` validates `@NotBlank name`, `@NotNull type`. Email/phone are free text server-side.
3. `VendorService.createVendor` builds a `Vendor`, stamps `institutionKey` from the principal, and saves. `uuid`, `created_at`, `updated_at` are DB/entity-generated (`@GeneratedValue`, `@CreationTimestamp`, `@UpdateTimestamp`).
4. Returns `201 CREATED` with the `VendorResponse`. Frontend re-runs `load()` on `onCreated`.

## Persistence

Single table, `hiveiq_claims` DB (Flyway `V1__init_schema.sql`):

| Column | Type | Notes |
|--------|------|-------|
| `id` | BIGSERIAL PK | internal FK target |
| `uuid` | UUID UNIQUE | public identifier in API |
| `name` | VARCHAR(255) NOT NULL | required |
| `type` | VARCHAR(3) NOT NULL | CHECK IN ('CIT','SP') |
| `contact_name` / `contact_email` / `contact_phone` | VARCHAR | optional |
| `institution_key` | VARCHAR(255) | scoping key; **nullable, no index** |
| `created_at` / `updated_at` | TIMESTAMPTZ | defaults NOW() |

- **Referenced by** `claims.cit_vendor_id` and `claims.sp_vendor_id` (`BIGINT REFERENCES vendors(id)`). This FK is why vendors have no delete endpoint — a referenced vendor can't be removed. Also referenced conceptually by `claim_messages.vendor_id` / `claim_attachments.vendor_id`.
- No unique constraint on `(institution_key, name)` — duplicates are allowed.
- `dev` profile = H2 in-memory (`create-drop`, Flyway off); `prod` = Postgres + Flyway `validate`.

## API endpoints (this feature only)

| METHOD | Path | Handler |
|--------|------|---------|
| GET | `/api/claims/vendors` (`?type=CIT|SP`) | `VendorController.listVendors` |
| POST | `/api/claims/vendors` | `VendorController.createVendor` |

Externally routed as `api.bcos.cloud/claims/api/claims/vendors`. Both require a valid JWT; there is no role authority check (see [claims.vendors] Internal tab).

## Kafka

None for vendors. (The service-level `hiveops.device.events` consumer feeds `device_summary` for claim filing, not the registry.) See [platform.kafka].

## Design gaps worth knowing

- Create/read only — no update, no delete, no dedup (all confirmed absent in `VendorController`/`VendorRepository`).
- Scoping relies entirely on a correctly-populated JWT `institutionKey`; a blank claim silently pools vendors under a null key.
