---
module: technical.vault
title: Vault — CIT visit tracking & cash count service
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01 from hiveops-vault source. Verify against code before relying on specifics.

## Responsibility

`hiveops-vault` records CIT (Cash in Transit) service visits on ATMs — what the CIT vendor finds, adds, and removes during a replenishment. It is the CIT-side data source for cash reconciliation: per-cassette counts (opening/added/removed/closing), non-cash removals (checks, deposits, rejected notes), and CSV bulk import of vendor files. It maintains a local read-only mirror of device metadata (`device_summary`) fed by Kafka. See [platform.service-ownership].

Combined repo: Spring Boot backend (`backend/`, port 8094) + Svelte SPA (`frontend/`, port 5185, `vault.bcos.cloud`).

## Data

PostgreSQL database **`hiveiq_vault`** (prod profile). See [platform.data-stores].

- Env vars are the split form `DB_HOST` / `DB_PORT` (default 5432) / `DB_NAME` (default `hiveiq_vault`) / `DB_USERNAME` / `DB_PASSWORD` — **not** the `DB_URL` form some other services use.
- Dev profile runs H2 in-memory (`create-drop`, Flyway disabled); prod runs PostgreSQL with `ddl-auto: validate` + Flyway migrations.

Key tables (Flyway `V1__create_vault_schema.sql`):

| Table | Entity | Purpose |
|-------|--------|---------|
| `cit_visits` | `CitVisit` | One CIT service call on an ATM; status `PENDING → CONFIRMED / DISPUTED` |
| `cit_cassette_counts` | `CitCassetteCount` | Per-slot counts (denomination, `cassette_type` DISPENSER/RECYCLER/RETRACT/MIX, opening/added/removed/closing) |
| `cit_removals` | `CitRemoval` | Non-cash items removed: `CHECK`, `DEPOSIT`, `REJECTED_NOTE` |
| `cit_imports` | `CitImport` | CSV import audit log; status `PROCESSING → COMPLETED / FAILED` |
| `device_summary` | `DeviceSummary` | Device metadata mirror populated from Kafka (added V3/V5 migrations) |

All rows are scoped by `institution_key`.

## Kafka

**Consumed:** `hiveops.device.events` — listener `DeviceEventConsumer` (config `DeviceEventKafkaConfig`, consumer group `hiveops-vault-device-sync`, offset reset `earliest`). Upserts/deletes rows in `device_summary`; `DEVICE_DELETED` events remove the record. Payload type `DeviceEventMessage`.

**Produced:** None (no `KafkaTemplate`/`StreamBridge` in the codebase).

See [platform.kafka].

## API surface

Base path **`/api/vault`**. All endpoints require a JWT Bearer token (validated by `JwtAuthenticationFilter` → `VaultPrincipal`). Results are scoped to the caller's `institutionKey`.

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/vault/visits` | List visits, paginated + filters (`atmId`, `vendor`, `status`, `from`, `to`) |
| GET | `/api/vault/visits/{id}` | Visit detail (cassette counts + removals) |
| POST | `/api/vault/visits` | Create a visit manually (201) |
| PUT | `/api/vault/visits/{id}` | Update status / notes / vendor |
| DELETE | `/api/vault/visits/{id}` | Delete a visit (204) |
| GET | `/api/vault/visits/recent` | Recent visits for dashboard |
| GET | `/api/vault/atm/{atmId}/visits` | All visits for one ATM |
| GET | `/api/vault/atms` | Device list (from `device_summary` mirror) for the institution |
| POST | `/api/vault/imports` | Upload CSV multipart `file` → async processing (202 Accepted) |
| GET | `/api/vault/imports` | Import history (paginated) |
| GET | `/api/vault/imports/{id}` | Single import status (poll while PROCESSING) |
| GET | `/api/vault/stats` | Dashboard aggregate stats |
| GET | `/api/vault/users/me` | Current user info decoded from JWT |

## Gotchas

- **No role checks.** `SecurityConfig` only requires `authenticated()` on `anyRequest()` — there is no `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` anywhere. Authorization is purely institution-scoping via `VaultPrincipal.institutionKey()`, not role gating.
- **Spring-level CORS is disabled** (`.cors(...::disable)`); CORS is handled upstream by NGINX. NGINX maps `api.bcos.cloud/vault/` → `hiveiq-vault:8094/api/vault/`.
- **MSP_ADMIN create path:** on `POST /visits`, when the principal has no `institutionKey` (multi-institution admin), the institution is derived from the visited ATM via `deviceSummaryRepository.findByAtmId(...)`.
- **device_summary is a Kafka-fed mirror, not owned data** — vault never writes device metadata except from the `hiveops.device.events` consumer. New devices with no `deviceId` in the event are skipped on insert; missing `institutionKey` defaults to `"BCOS"`. If the mirror is stale, it's an upstream (devices) publish gap, not a vault bug.
- **CSV upload validation** is content-type based (`csv`/`text` substring) and processing is `@Async` — the POST returns 202 immediately; clients must poll `/imports/{id}` for terminal `COMPLETED`/`FAILED`.
- **Uploads dir** defaults to `/tmp/vault-uploads` (`UPLOADS_DIR`); multipart max file size 20MB.
