---
module: reports.templates
title: Report Templates
tab: Architect
order: 30
audience: dev
---

> **Internal — architecture.** How the Report Templates feature is built. Owner: **hiveops-reports** (see [platform.service-ownership]).

## Services involved
| Service | Role for this feature |
|---|---|
| **hiveops-reports** | Owns template CRUD, scheduling, run execution, export, distribution. All template state lives here. |
| hiveops-incident | Data source at **run** time (`INCIDENTS`, `ATM_HEALTH`, `FLEET_TASKS`, `AGENT_VERSIONS`, `AGENT_MODULES`, `SOFTWARE_ERRORS`) |
| hiveops-journal | Data source at run time (`TRANSACTIONS`) |
| hiveops-messaging | In-app delivery of completed scheduled reports |
| hiveops-mgmt | Recipient resolution / user context (downstream) |

Templates themselves are **pure DB CRUD inside hiveops-reports** — no other service is touched until a template is *run*.

## Data model (DB `hiveiq_reports`, Postgres prod / H2 dev)
See [platform.data-architecture]. Flyway `V1`–`V10`.

| Table | Written by | Notes |
|---|---|---|
| `report_templates` | `ReportTemplateService` | The template definition. `columns`/`filters`/`group_by_fields`/`aggregations`/`distribution_config` are `jsonb`. `data_source` enum, `output_format` enum. Ownership `created_by_user_id`; `institution_key` (V7), `shared` flag (V5), `schedule_query_id` link (V3/V4). |
| `report_queries` | `ReportQueryService` | Named saved date-range + ATM/device sets attached to a template (V3). The scheduled query is referenced by `report_templates.schedule_query_id`. |
| `report_runs` | `ReportRunService` | Execution instances spawned from a template. |
| `report_distribution_log` | `ReportDistributorService` | Per-recipient delivery record. |
| `device_summary` | `DeviceEventConsumer` (Kafka) | Local device mirror (V8/V10) backing the run-time ATM picker; not part of the template row. |

Seed: `V6` inserts a shared `SOFTWARE_ERRORS` template ("Software Error Pattern Analysis", cron Monday 08:00 UTC).

## Template lifecycle (data flow)
```
Create/Update (REST)
  ReportTemplateController → ReportTemplateService.create/update
    → validate data source via SchemaRegistry.find()
    → persist report_templates (jsonb columns)
    → if scheduleEnabled: next_run_at = now()   ← fires on next scheduler tick

Run — user-triggered
  POST /templates/{id}/run
    → ReportRunController extracts caller Bearer
    → ReportRunService.triggerUserRun (loads template by owner OR shared)
    → @Async: fetcher (incident/journal) with FORWARDED caller JWT
    → ReportExportService: project columns → groupBy/aggregate → CSV/JSON
    → report_runs status PENDING→RUNNING→COMPLETED/FAILED

Run — scheduled
  ReportSchedulerService @Scheduled(fixedDelay=60s)
    → templateRepository.findScheduledDue(now)
    → InternalTokenGenerator.generateForInstitution(template.institutionKey)  (self-minted ADMIN JWT)
    → ReportRunService.triggerScheduledRun(template, serviceToken)
    → recompute next_run_at from schedule_cron (Spring CronExpression, UTC)
    → on COMPLETED: ReportDistributorService delivers + emits Kafka
```

## Executor → Kafka → record-store pattern
The run executor (`ReportRunService`) is the async worker (pool core=2/max=5 per `AsyncConfig`). On completion, `ReportDistributorService`:
- POSTs the completed report as an in-app message to **hiveops-messaging** (`/api/v1/messages`), writing `report_distribution_log` (the record store), and
- publishes to Kafka topic **`hiveops.reports.completed`** (producer in `ReportsKafkaConfig`; key = `institutionKey` or `"global"`).

See [platform.kafka].

## Kafka topics (feature-relevant)
| Topic | Direction | Purpose |
|---|---|---|
| `hiveops.reports.completed` | **Produced** | Emitted after a run (incl. scheduled template runs) completes/distributes |
| `hiveops.device.events` | **Consumed** (`DeviceEventConsumer`, group `hiveops-reports-device-sync`, offset `earliest`) | Upserts/deletes `device_summary` mirror used by the run-time ATM picker; no reconciler — if the topic stops, device list goes stale |

Template CRUD itself produces **no** Kafka; Kafka only appears at run/distribute time.

## Key API endpoints (all under hiveops-reports, JWT required)
| Method | Path | Purpose |
|---|---|---|
| GET | `/api/reports/templates` | List caller's templates (pageable) |
| POST | `/api/reports/templates` | Create |
| GET/PUT/DELETE | `/api/reports/templates/{id}` | Get/update/delete (owner only) |
| GET | `/api/reports/templates/shared` | Templates shared with caller |
| POST / DELETE | `/api/reports/templates/{id}/share` | Share / unshare |
| POST | `/api/reports/templates/{id}/run` | Async run → `202` + run id |
| GET/POST/PUT/DELETE | `/api/reports/templates/{templateId}/queries[...]` | Saved queries CRUD |
| POST | `.../queries/{queryId}/set-scheduled` | Mark the scheduled query |
| DELETE | `.../queries/scheduled` | Clear scheduled query |
| GET | `/api/reports/schema[/{dataSource}]` | Field/filter definitions used by the builder UI |

## Auth model
- **User runs:** forward the caller's `Authorization: Bearer` to downstream.
- **Scheduled runs:** `InternalTokenGenerator` mints a short-lived ADMIN JWT signed with shared `JWT_SECRET` (`Keys.hmacShaKeyFor(secret.getBytes(UTF_8))`), scoped to the template's `institution_key`.
- Filter chain (`JwtAuthenticationFilter` + `SecurityConfig`) is authenticate-only; authorization is enforced in-service via ownership queries, not roles. See [platform.service-ownership].
