tr-engine

April 1, 2026 · View on GitHub

Backend service that ingests data from trunk-recorder instances and serves it via a REST API with real-time streaming. Handles radio system monitoring data: calls, talkgroups, units, transcriptions, live audio, and recorder state.

Zero configuration for radio systems — tr-engine discovers systems, sites, talkgroups, and units automatically. Point it at a broker, a watch directory, or a trunk-recorder install, give it a database, and it figures out the rest.

Note: This is a ground-up rewrite of the original tr-engine, now archived at LumenPrima/tr-engine-v0. The database schema is not compatible. If you're coming from v0, see the migration guide.

Screenshots

Live demo: tr-dashboard.luxprimatech.com

Talkgroup Research — Browse View

Browse all discovered talkgroups with search, system filtering, and sortable columns. Card grid and list views available.

Browse view — Crystal theme

Talkgroup Research — Detail View

Click any talkgroup to see stats, 24-hour activity timeline, site distribution, and encryption indicator.

Detail view — Crystal theme

Units Tab — Top Talkers & Network Graph

Horizontal bar chart of most active units plus an interactive SVG network graph showing unit-to-talkgroup relationships.

Units tab — Crystal theme

Dark Theme (Night City)

All pages support 11 switchable themes. Here's the detail view with activity chart and calls tab.

Detail view — Night City theme Units tab — Night City theme

Tech Stack

  • Go — multi-core utilization at high message rates
  • PostgreSQL 17+ — partitioned tables, JSONB, denormalized for read performance
  • MQTT + File Watch — ingests from trunk-recorder via MQTT or filesystem monitoring (or both)
  • REST API — 80+ endpoints under /api/v1, defined in openapi.yaml
  • SSE — real-time event streaming with server-side filtering
  • Live Audio — UDP simplestream ingest with per-talkgroup Opus encoding, WebSocket delivery
  • Transcription — pluggable STT providers (Whisper, ElevenLabs, DeepInfra, IMBE ASR)
  • Web UI — built-in dashboards and companion tr-dashboard React app

Quick Start

Run this from your trunk-recorder directory (requires Docker):

curl -sL https://raw.githubusercontent.com/trunk-reporter/tr-engine/master/install.sh | sh

That's it. Open http://localhost:8080 — call recordings will appear as trunk-recorder captures them.

To remove: cd tr-engine && docker compose down -v && cd .. && rm -rf tr-engine

Other Installation Methods

Updating

docker compose pull && docker compose up -d

Database and audio files persist in Docker volumes across updates.

Authentication

tr-engine has three auth modes, determined by which environment variables you set:

ConfigModeBehavior
Neither AUTH_TOKEN nor ADMIN_PASSWORDOpenNo auth — all endpoints accessible
AUTH_TOKEN setTokenShared API token required for all access
ADMIN_PASSWORD setFullJWT login with role-based access. Optional public read access via AUTH_TOKEN.

The GET /api/v1/auth-init endpoint returns the current auth mode so clients (tr-dashboard, web UI) can automatically detect what's needed — no proxy injection or manual config required.

For public-facing deployments: Set both AUTH_TOKEN (public read access) and ADMIN_PASSWORD (admin login for writes). Put behind a reverse proxy with TLS.

For private/local use: Set AUTH_TOKEN for basic protection, or leave both unset for open access.

See Auth Migration Guide if upgrading from WRITE_TOKEN/AUTH_ENABLED (both deprecated).

Configuration

Configuration is loaded in priority order: CLI flags > environment variables > .env file > defaults.

The .env file is auto-loaded from the current directory on startup. See sample.env for all available fields.

CLI Flags

--listen        HTTP listen address (default :8080)
--log-level     debug, info, warn, error (default info)
--database-url  PostgreSQL connection URL
--mqtt-url      MQTT broker URL
--audio-dir     Audio file directory (default ./audio)
--watch-dir     Watch TR audio directory for new files
--tr-dir        Path to trunk-recorder directory for auto-discovery
--env-file      Path to .env file (default .env)
--version       Print version and exit

Key Environment Variables

VariableRequiredDefaultDescription
DATABASE_URLYesPostgreSQL connection string
MQTT_BROKER_URL*MQTT broker URL (e.g., tcp://localhost:1883)
WATCH_DIR*Watch TR audio directory for new files
TR_DIR*Path to trunk-recorder directory for auto-discovery
MQTT_TOPICSNo#MQTT topic filter (match your TR plugin prefix with /#)
HTTP_ADDRNo:8080HTTP listen address
AUTH_TOKENNoShared API token (token mode) or public read token (full mode)
ADMIN_PASSWORDNoEnables JWT login, seeds admin user on first run
CORS_ORIGINSNo*Comma-separated allowed CORS origins
RATE_LIMIT_RPSNo20Per-IP rate limit (requests/second)
AUDIO_DIRNo./audioAudio file storage directory
STT_PROVIDERNowhisperTranscription provider: whisper, elevenlabs, deepinfra, imbe
STREAM_LISTENNoUDP listen address for live audio (e.g., :9123)
LOG_LEVELNoinfoLog level

* At least one of MQTT_BROKER_URL, WATCH_DIR, or TR_DIR must be set. All three can run simultaneously.

See sample.env for the full list including MQTT credentials, HTTP timeouts, transcription tuning, S3 storage, and retention settings.

Audio Modes

tr-engine supports two modes for call audio:

  • MQTT audio (default): trunk-recorder sends base64-encoded audio in MQTT messages. tr-engine decodes and saves the files to AUDIO_DIR. Enable with mqtt_audio: true in trunk-recorder's MQTT plugin config.

  • Filesystem audio (TR_AUDIO_DIR): trunk-recorder saves audio to its local filesystem. tr-engine serves them directly. Set TR_AUDIO_DIR to trunk-recorder's audioBaseDir. When using this mode, set mqtt_audio_type: none in the TR plugin config to skip base64 encoding.

Both modes can coexist during a transition.

How It Works

Ingest Modes

tr-engine supports four ingest modes that can run independently or simultaneously:

  • MQTT — subscribes to trunk-recorder's MQTT status plugin for real-time call events, unit activity, recorder state, decode rates, trunking messages, and console logs. The richest data source.
  • File Watch (WATCH_DIR) — monitors trunk-recorder's audio output directory for new .json metadata files. Only produces call_end events. Backfills existing files on startup (WATCH_BACKFILL_DAYS).
  • TR Auto-Discovery (TR_DIR) — the simplest setup. Point at trunk-recorder's directory. Auto-discovers capture directory, system names, imports talkgroup and unit CSVs. With CSV_WRITEBACK=true, alpha_tag edits are written back to the CSV files.
  • HTTP Upload (POST /api/v1/call-upload) — accepts multipart uploads compatible with trunk-recorder's rdio-scanner and OpenMHz upload plugins. No local audio capture or MQTT broker required. Authenticates via API key (tre_ prefix), bearer token, or form field key.

Auto-Discovery

tr-engine builds its model of the radio world automatically from incoming messages:

  1. Identifies systems by matching P25 (sysid, wacn) pairs or conventional (instance_id, sys_name)
  2. Discovers sites within each system — multiple TR instances monitoring the same P25 network auto-merge into one system with separate sites
  3. Tracks talkgroups and units as they appear in call and unit events
System "MARCS" (P25 sysid=348, wacn=BEE00)
  |- Site "butco"  (nac=340, instance=tr-1)
  |- Site "warco"  (nac=34D, instance=tr-2)
  |- Talkgroups (shared across all sites)
  +- Units (shared across all sites)

Data Flow

trunk-recorder  ──MQTT──>  broker  ──MQTT──>  tr-engine  ──REST/SSE──>  clients
      |                                            |
      +──audio files──>  fsnotify watcher ─────────+
      |                                            |
      +──HTTP upload──>  POST /call-upload ────────+
      |                                            |
      +──simplestream UDP──> audio router ─────────+──WebSocket──> live audio
                                                   v
                                               PostgreSQL

Real-Time Event Streaming

GET /api/v1/events/stream pushes filtered events over SSE.

  • Filter params (all optional, AND-ed): systems, sites, tgids, units, types, emergency_only
  • 8 event types: call_start, call_update, call_end, unit_event, recorder_update, rate_update, trunking_message, console
  • Compound type syntax: types=unit_event:call filters by subtype
  • Reconnect: Last-Event-ID header for gapless recovery (60s server-side buffer)

Live Audio Streaming

GET /audio/live delivers real-time radio audio via WebSocket.

  • UDP ingest from trunk-recorder's simplestream plugin (STREAM_LISTEN)
  • Per-talkgroup Opus encoding (configurable bitrate, PCM passthrough option)
  • Multi-site deduplication — same call from multiple sites sent once
  • Subscribe/unsubscribe filtering by system IDs and talkgroup IDs
  • Browser playback via AudioWorklet (audio-engine.js + audio-worklet.js)

Transcription

Pluggable speech-to-text with four providers:

ProviderConfigNotes
WhisperSTT_PROVIDER=whisper + WHISPER_URLSelf-hosted or cloud Whisper-compatible API
ElevenLabsSTT_PROVIDER=elevenlabs + ELEVENLABS_API_KEYElevenLabs Scribe API
DeepInfraSTT_PROVIDER=deepinfra + DEEPINFRA_STT_API_KEYHosted Whisper models
IMBE ASRSTT_PROVIDER=imbe + IMBE_ASR_URLTranscribes directly from P25 IMBE codec frames via DVCF

Features: configurable worker pool, queue size, duration filters, anti-hallucination parameters, provider_ms performance tracking, talkgroup include/exclude filtering.

API

80+ endpoints under /api/v1. See openapi.yaml for the full specification, or open the built-in Swagger UI at /docs.html.

Key Endpoints

EndpointDescription
GET /healthService health, TR instance status, version
GET /auth-initAuth mode discovery (open/token/full)
GET /systemsList radio systems
GET /talkgroupsList talkgroups (filterable, sortable)
GET /unitsList radio units
GET /callsCall recordings (paginated, filterable)
GET /calls/activeCurrently in-progress calls
GET /calls/{id}/audioStream call audio
GET /calls/{id}/transcriptionCall transcription
GET /transcriptions/searchFull-text search across transcriptions
GET /unit-eventsUnit event queries
GET /unit-affiliationsLive talkgroup affiliation state
GET /call-groupsDeduplicated call groups across sites
GET /recordersRecorder hardware state
GET /events/streamReal-time SSE event stream
GET /audio/liveLive audio WebSocket
GET /statsSystem statistics
GET /talkgroup-directoryTalkgroup reference directory
POST /call-uploadUpload call recording (rdio-scanner/OpenMHz)
POST /queryAd-hoc read-only SQL queries
POST /admin/systems/mergeMerge duplicate systems
POST /debug-reportSubmit diagnostic report

Web UI

tr-engine ships with built-in dashboards at http://localhost:8080. The index page auto-discovers all pages.

PageDescription
Event HorizonLogarithmic timeline — events drift from now into the past
OmniTrunkerReal-time system overview with active calls, recorders, and decode rates
Live EventsReal-time SSE event stream with type filtering
Unit TrackerLive unit status grid with state colors and group filters
IRC Radio LiveIRC-style monitor — talkgroups as channels, units as nicks, audio playback
ScannerMobile-friendly radio scanner with auto-play and channel filtering
Talkgroup ResearchDeep-dive analysis — browse, detail charts, unit network graph, call history with audio
Talkgroup DirectoryBrowse and import talkgroup reference data from CSV
Call HistorySearchable call log with inline audio playback and transmission timeline
TimelineInvestigation timeline with talkgroup rows and call blocks
Systems OverviewSystem and site health dashboard
Signal FlowStream graph of talkgroup activity over time (D3.js)
AnalyticsSystem-wide statistics and trends
AdminUser management, API keys, maintenance controls
API DocsInteractive Swagger UI for the REST API
Page BuilderGenerate custom dashboard pages with AI assistance

Pages are plain HTML with no build step. Add new pages by dropping an .html file in web/ with a <meta name="card-title"> tag — see CLAUDE.md for the spec.

tr-dashboard

For a full-featured React dashboard with talkgroup favorites, call playback, unit investigation, and live audio, see tr-dashboard. It connects to tr-engine's API and auto-detects auth mode via /api/v1/auth-init.

Storage Estimates

Observed with 2 moderately busy counties and 1 trunk-recorder instance:

CategoryEstimated Annual Usage
Database (permanent tables)~22 GB/year
Database (state + logs overhead)~3 GB steady-state
Audio files (M4A)~140 GB/year

High-volume tables (calls, unit_events, trunking_messages) are automatically partitioned by month. Partition maintenance runs daily, creating partitions 3 months ahead. State tables are decimated (1/min after 1 week, 1/hour after 1 month). Configurable retention via RETENTION_* env vars.

Project Structure

cmd/tr-engine/main.go           Entry point with CLI flag parsing
internal/
  config/config.go              .env + env var + CLI config loading
  database/                     PostgreSQL connection pool + query files
  mqttclient/client.go          MQTT client with auto-reconnect
  ingest/
    pipeline.go                 MQTT message dispatch + batchers
    router.go                   Topic-to-handler routing
    identity.go                 System/site identity resolution + caching
    eventbus.go                 SSE pub/sub with ring buffer replay
    watcher.go                  fsnotify-based file watcher
    handler_*.go                Per-topic message handlers
  audio/
    simplestream.go             UDP listener for trunk-recorder simplestream
    router.go                   Identity resolution, dedup, encoding
    bus.go                      Pub/sub for audio frames
  transcribe/                   STT worker pool + provider implementations
  trconfig/
    trconfig.go                 TR config.json, docker-compose, and CSV parsers
    discover.go                 TR auto-discovery orchestrator
  api/
    server.go                   Chi router + HTTP server
    middleware.go               Auth, rate limiting, CORS, body limits
    events.go                   SSE event stream endpoint
    audio_stream.go             WebSocket live audio endpoint
    *.go                        Handler files for each resource
web/                            Built-in dashboards (auto-discovered by index)
openapi.yaml                    API specification (source of truth)
schema.sql                      PostgreSQL DDL (auto-applied on first run)
sample.env                      Configuration template

Roadmap

See the Trunk Reporter Roadmap for the cross-repo project tracker.

License

MIT