---
module: technical.transactions
title: Transactions — stateless proxy gateway for the transaction browser SPA
tab: Overview
order: 10
audience: dev
---

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

## Responsibility

`hiveops-transactions` is a thin, stateless **backend-for-frontend proxy** for the transaction-browser / customer-journey SPA (`transactions.bcos.cloud`, port 5178). It owns no domain data of its own: it authenticates the caller's JWT, then forwards `/api/**` requests to the services that actually own the data — journal, devices, and incident/Adoons. It exposes one first-party endpoint, `/api/users/me`, derived from the JWT claims. See [platform.service-ownership].

## Data

**No database.** The service has no `@Entity`, no JPA config, and no Flyway migrations — it holds no persistent state. Transaction data lives in the journal service DB; ATM/device data in the devices service DB. See [platform.data-stores].

## Kafka

No Kafka. No producers, consumers, or `@KafkaListener` in the codebase.

## API surface

Server port **8098** (`application.properties`). All routes require a valid JWT (`hasAnyRole(USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN)`) except `/actuator/health` and `/error`.

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/users/me` | First-party: current user (email, role, institutionKey(s), fleetTaskApprover) from JWT — `UserController` |
| ANY | `/api/journal`, `/api/journal/**` | Proxy → journal service (`services.journal.url`) — `JournalProxyController` |
| ANY | `/api/atms`, `/api/atms/**` | Proxy → devices service — `DevicesProxyController` |
| ANY | `/api/atm-groups`, `/api/atm-groups/**` | Proxy → devices service — `DevicesProxyController` |
| ANY | `/api/adoons`, `/api/adoons/**` | Proxy → AI/Adoons endpoints (`services.ai.url`) — `AdoonsProxyController` |

The three proxy controllers accept any HTTP method (`@RequestMapping` without a `method` restriction) and forward the original path + query string verbatim via `ProxyService.forward()`.

**Upstream targets** (`AppProperties` / `application.properties`):
- `services.journal.url` → `JOURNAL_URL` (default `http://hiveiq-journal:8087`)
- `services.devices.url` → `DEVICES_URL` (default `http://hiveiq-devices-backend:8096`)
- `services.ai.url` → `AI_URL` (default `http://hiveiq-incident:8081`)

## Gotchas

- **`services.ai.url` points at incident, not the AI service.** Default is `http://hiveiq-incident:8081`; incident owns the `/api/adoons/**` endpoints (per the comment in `AdoonsProxyController`). Don't assume Adoons traffic hits hiveops-ai directly.
- **Role check is broad.** `SecurityConfig` gates `/api/**` with `hasAnyRole("USER","CUSTOMER","ADMIN","MSP_ADMIN","QDS_ADMIN","BCOS_ADMIN")` — not the `MSP_ADMIN`/`BCOS_ADMIN`-only pattern used elsewhere. Authorization beyond role is delegated to the upstream services.
- **Only `Authorization` and `Content-Type` headers are forwarded.** `ProxyService` rebuilds a fresh `HttpHeaders` — other inbound headers (custom `X-*`, cookies) are dropped. The `transfer-encoding` response header is stripped from replies.
- **CORS is disabled at the app** (`.cors(AbstractHttpConfigurer::disable)`); nginx is the CORS authority. Allowed origins config key (`cors.allowed-origins`, default `http://localhost:5178`) exists but is not wired into the disabled security chain.
- **JWT secret is required** — `jwt.secret=${JWT_SECRET}` has no default; the service will fail to start without it.
- `/api/users/me` reads the institution key(s) from the auth credentials: a single-element list → `institutionKey`, multiple → `institutionKeys`; presence of a `FLEET_APPROVER` authority sets `fleetTaskApprover`.
