Open-source, self-hosted admin console for NATS JetStream — github.com/gopherust-io/nats-console.
Manage streams, consumers, browse messages, tail live traffic, manage KV/Object stores, and monitor multi-cluster deployments from a modern web UI — without exposing NATS monitoring ports to the public internet.
Latest stable release: v0.11.0 · all releases
Latest stable release: see GitHub Releases.
Quick links: Architecture · Getting started · User guide · DevOps setup · Security
📖 Documentation: friendly guides for everyone — docs/README.md
- Multi-cluster registry with PostgreSQL persistence
- Multi-tenant RBAC — operator/viewer/admin scoped by
accessRules.clusterIds - Slow consumer detection — pending/lag/ack-pending thresholds, UI badges, Topology warnings, alert metrics
- Incident Capsule Studio — capture/list/load forensic packs via
gopherust-io/natsIncidents()(Object Store + KV index); DLQ “Capture capsule” + consumer panel with dry-run replay. Distinct from Audit incident reconstruction (Postgres timeline annotations). - Historical metrics — Postgres snapshots + Dashboard trend charts
- Message publish — publish to JetStream streams from UI and API
- Encryption key rotation — root API to re-encrypt stored credentials
- Enterprise security — AES-GCM credential encryption, audit log, username/password auth and invite links, hardened pprof
- Dashboard with JetStream account usage, server info, and jsz metrics
- Stream list, create, update, delete, purge
- Consumer CRUD with detail pages
- Message browser with prev/next navigation and JSON/raw view
- Live mode — real-time WebSocket tail per stream (race-safe hub)
- KV Store and Object Store management
- On-demand profiling — admin-only
/api/v1/pprof/*(raw/debug/pprofdisabled in production) - OTLP process telemetry via tel (HTTP/NATS counters); JetStream history via
/api/v1/clusters/{id}/metrics/history - Helm chart for Kubernetes deployment
- OpenAPI spec served at
/api/openapi.yaml - Docker Compose quickstart with NATS + PostgreSQL + JetStream
git clone https://github.com/gopherust-io/nats-console.git
cd nats-console
cp .env.example .env # set ADMIN_PASSWORD and other secrets
docker compose up --buildOpen http://localhost:8080 and sign in:
- Basic auth:
admin/admin(or your.envADMIN_*values) - People invite: Admin → People → create invite link →
/invite/<token>sets password
For production, terminate TLS on an external reverse proxy or Ingress in front of :8080 (see devops-setup.md).
NATS is exposed locally on:
- Client:
nats://localhost:4222 - Monitoring:
http://localhost:8222
Local JetStream labs (single / 5-node cluster / supercluster / auth): docker/nats/ — see docs/local-docker.md. For the Replicas page use make nats-cluster-up. Stop the compose nats service first when ports clash.
PostgreSQL: set DATABASE_URL in .env (see .env.example). Stock compose is a local plaintext lab only.
On first startup, a default cluster is seeded from NATS_URL / NATS_MONITORING_URL (and optional NATS_TOKEN / NATS_CREDS_FILE) when the registry is empty.
- Start with
docker compose up --build. - Open
http://localhost:8080and sign in as admin. - Create a stream, publish a test message, then open Live mode.
- Verify dashboard counters and stream/consumer updates refresh immediately.
If you want visuals in the repo page, add screenshots to docs/images/ and link them from this section.
Browser → NATS Consol fasthttp (:8080) → PostgreSQL
→ NATS JetStream (4222)
→ NATS Monitoring (8222)
flowchart LR
Browser[BrowserUI] --> Api[NatsConsolAPI]
Api --> Pg[PostgreSQL]
Api --> Js[NatsJetStream]
Api --> Mon[NatsMonitoring]
The UI never talks to NATS directly. The backend acts as a secure gateway. All JetStream operations are cluster-scoped:
/api/v1/clusters/{clusterId}/streams
/api/v1/clusters/{clusterId}/live/ws
/api/v1/clusters/{clusterId}/kv/buckets
/api/v1/clusters/{clusterId}/objects/buckets
-
ENV=production - Strong
ADMIN_PASSWORD,ENCRYPTION_KEY, and RSA session key pair (SESSION_PRIVATE_KEY/SESSION_PUBLIC_KEY) - TLS configured end-to-end (
https://base URL and secure NATS/monitoring endpoints) - PostgreSQL with backups, retention, and restricted network access
- Audit log retention and alerting on auth failures / critical mutations
- CI + regression suites passing before deployment
Requirements:
- Go 1.26+
- Node.js 22+
- PostgreSQL 16+
- NATS Server with JetStream enabled
Start dependencies:
cp .env.example .env # required — set passwords and secrets
docker compose up postgres nats -dBackend:
# Prefer values from .env; for host-run against published ports:
export DATABASE_URL=postgres://natsconsol:${POSTGRES_PASSWORD}@localhost:5432/natsconsol?sslmode=disable
export NATS_URL=nats://localhost:4222
export NATS_MONITORING_URL=http://localhost:8222
export ADMIN_PASSWORD=change-me-local-admin
export ENCRYPTION_KEY=dev-encryption-key-min-16-chars
# Generate once: openssl genrsa -out session.pem 2048 && openssl rsa -in session.pem -pubout -out session.pub.pem
# Then set SESSION_PRIVATE_KEY / SESSION_PUBLIC_KEY (PEM; literal \n escapes OK)
export SESSION_PRIVATE_KEY="$(awk 'NF {sub(/\r/,""); printf "%s\\n",$0}' session.pem)"
export SESSION_PUBLIC_KEY="$(awk 'NF {sub(/\r/,""); printf "%s\\n",$0}' session.pub.pem)"
go generate ./... # after changing internal/config/config.go
go run ./cmdConfig is loaded via gopherust-io/env (envgen codegen). Install the generator once:
go install github.com/gopherust-io/env/cmd/envgen@latestFrontend:
cd web
npm install
npm run devFrontend dev server: http://localhost:8080 (proxies /api to console on :8081 when using make dev-web-docker; local make dev-web expects the Go API on :8081)
Backend (requires golangci-lint v2+):
make lint-go
make lint-go-fix # auto-fix (modernize, fieldalignment, tagalign, etc.)Enabled Go linters include modernize, govet/fieldalignment (struct layout), errorlint, gosec, exptostd, intrange, perfsprint, tagalign, embeddedstructfieldcheck, and the standard set (staticcheck, errcheck, unused, …). Config: .golangci.yml.
Frontend (uses local npm when available, otherwise Docker):
make lint-web
# or explicitly via Docker:
make lint-web-dockerBoth:
make lint| Variable | Default | Description |
|---|---|---|
HTTP_ADDR |
:8080 |
Console HTTP listen address |
HTTP_READ_TIMEOUT |
10s |
HTTP server read timeout |
HTTP_WRITE_TIMEOUT |
30s |
HTTP server write timeout |
HTTP_IDLE_TIMEOUT |
60s |
HTTP server idle timeout |
NATS_TOKEN |
— | NATS auth token for default cluster bootstrap |
NATS_ACCOUNT_SEED |
— | Optional account NKey seed for minting NATS user JWTs |
PUBLIC_BASE_URL |
http://localhost:8080 |
Public base URL for invite links |
ENV |
development |
Set to production to enforce TLS/auth secrets |
DATABASE_URL |
— (required) | PostgreSQL DSN; production requires sslmode=require|verify-ca|verify-full |
DB_MAX_CONNS |
25 |
PostgreSQL connection pool max size |
DB_MIN_CONNS |
2 |
PostgreSQL connection pool min size |
DB_MAX_CONN_LIFETIME |
1h |
Max lifetime of a pooled connection |
DB_MAX_CONN_IDLE_TIME |
30m |
Max idle time before a connection is closed |
DB_HEALTH_CHECK_PERIOD |
1m |
Interval between pool health checks |
ENCRYPTION_KEY |
— | AES-GCM key for cluster tokens (required) |
SESSION_PRIVATE_KEY |
— | PEM RSA private key (≥2048-bit) for RS256 session JWTs (required) |
SESSION_PUBLIC_KEY |
— | PEM RSA public key matching the private key (required) |
SESSION_TTL |
15m |
Access JWT / session cookie lifetime |
REFRESH_TOKEN_TTL |
168h |
Opaque refresh cookie lifetime (bound to User-Agent + client IP fingerprint) |
DEFAULT_CLUSTER_NAME |
default |
Name for env-seeded default cluster |
NATS_URL |
— | NATS client URL for default cluster seed; production requires tls:// or wss:// |
NATS_CLIENT_CACHE_TTL |
5m |
How long to cache NATS client connections per cluster |
NATS_MONITORING_URL |
— | NATS monitoring URL; production requires https:// |
NATS_CREDS_FILE |
— | NATS credentials file (required in production if no token) |
NATS_TOKEN |
— | NATS auth token (required in production if no creds file) |
NATS_TLS_CA_FILE |
— | PEM CA file for NATS TLS verification |
NATS_TLS_CERT_FILE |
— | PEM client certificate for NATS mTLS |
NATS_TLS_KEY_FILE |
— | PEM client key for NATS mTLS (with cert) |
NATS_TLS_SERVER_NAME |
— | Override TLS server name for NATS |
NATS_TLS_INSECURE_SKIP_VERIFY |
false |
Skip NATS cert verify (forbidden in production) |
STATIC_DIR |
— | Path to built frontend (web/dist) |
ADMIN_USERNAME |
admin |
Bootstrap admin username |
ADMIN_PASSWORD |
— (required) | Bootstrap admin password (must not be admin in production) |
CORS_ALLOWED_ORIGINS |
— | Comma-separated allowed origins |
LOG_JSON |
false |
true for JSON logs (prod); false for console (local) |
LOG_LEVEL |
info |
Log level: trace, debug, info, warn, error, fatal |
TEL_COLLECTOR_GRPC_ADDR |
127.0.0.1:4317 |
OTLP/gRPC collector address for process metrics/traces (github.com/gopherust-io/tel) |
PPROF_ENABLED |
false |
Enable Go pprof on-demand endpoints (admin) |
PPROF_AUTH_ENABLED |
true |
Require admin auth for pprof |
PPROF_CPU_MAX_SECONDS |
120 |
Max CPU profile duration |
SMTP_ENABLED |
false |
Email console users when an alert first opens |
SMTP_HOST / SMTP_PORT / SMTP_FROM |
— / 587 / — |
SMTP settings (required when enabled) |
REQUEST_TIMEOUT |
10s |
Timeout for NATS/monitoring calls |
PAGINATION_DEFAULT_LIMIT |
100 |
Default page size for list APIs |
PAGINATION_MAX_LIMIT |
500 |
Maximum allowed page size for list APIs |
AUDIT_DEFAULT_LIMIT |
50 |
Default page size for audit log when limit is omitted |
LIVE_WS_MAX_MESSAGES |
1000 |
Max messages per live WebSocket session |
LIVE_WS_IDLE_TIMEOUT |
5m |
Close idle live WebSocket connections after |
LIVE_WS_RATE_LIMIT |
100ms |
Minimum interval between live message frames |
MAX_REQUEST_BODY_SIZE |
1048576 |
Maximum API request body size in bytes (1 MiB) |
HTTP_RESPONSE_COMPRESSION |
true |
Compress API/SPA responses (brotli/gzip) when the client accepts it |
AUTH_RATE_LIMIT |
10 |
Max auth attempts per IP per window |
AUTH_RATE_LIMIT_WINDOW |
1m |
Window for auth rate limiting |
SLOW_CONSUMER_PENDING_THRESHOLD |
1000 |
Pending msgs ≥ this → slow consumer |
SLOW_CONSUMER_LAG_THRESHOLD |
1000 |
Stream lag ≥ this → slow consumer |
SLOW_CONSUMER_ACK_PENDING_RATIO |
0.9 |
Ack-pending ≥ ratio × MaxAckPending → slow |
BEHAVIOR_FINGERPRINT_KV_BUCKET |
nats_consol_fingerprints |
KV bucket for consumer behavior fingerprints |
AI_ENABLED |
false |
Enable JetStream AI assistant (Gemini) |
AI_API_KEY |
— | Google Gemini API key |
AI_MODEL |
gemini-2.5-flash |
Gemini model name |
AI_MAX_TOKENS |
4096 |
Max response tokens |
AI_REQUEST_TIMEOUT |
60s |
LLM request timeout |
AI_CONTEXT_CACHE_TTL |
45s |
How long to cache JetStream context for the assistant |
AI_GEMINI_API_BASE |
https://generativelanguage.googleapis.com/v1beta |
Gemini API base URL |
Built-in assistant scoped only to NATS JetStream and this console. Uses Google Gemini with your API key — billing via your Google account. Message payloads, credentials, and database data are never sent to the model.
AI_ENABLED=true
AI_API_KEY=your-gemini-api-key
AI_MODEL=gemini-2.5-flashOpen the AI floating button in the console (bottom-right) after signing in.
API: POST /api/v1/clusters/{clusterId}/assistant/chat
| Role | Permissions |
|---|---|
| root | Single bootstrap superuser (is_root); full access; creates delegated admins with access rules |
| admin | Full access when unscoped (legacy), or limited via accessRules when created by root |
| operator | CRUD streams/consumers/KV/objects within assigned clusters |
| viewer | Read-only (dashboard, browse, live tail) within assigned clusters |
The bootstrap account (ADMIN_USERNAME / ADMIN_PASSWORD) is seeded as the root user on first start. Root can create additional admin users with configurable access rules. Operator and viewer users must be assigned at least one cluster via accessRules.clusterIds (multi-tenant scoping).
| Access rule | Meaning |
|---|---|
clusterIds |
Required for non-root users (except legacy unscoped admin). Limits API access to listed cluster UUIDs. Empty list = no cluster access. |
manageUsers |
Create, update, delete users and assign roles (delegated admin only) |
viewAudit |
Read the audit log |
assignableRoles |
Roles this admin may grant to others |
Migration (v0.5): After upgrading, assign clusterIds to existing operator/viewer (and scoped admin) accounts that previously had implicit access to all clusters. Users with empty clusterIds lose cluster access until clusters are assigned.
Root cannot be deleted or demoted by non-root users. Only one root account may exist.
NATS Consol applies defense-in-depth for browser and API traffic:
- HTTP headers —
Content-Security-Policy,X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy, andStrict-Transport-SecuritywhenPUBLIC_BASE_URLuses HTTPS. - Cookies — Session cookies are
HttpOnly,SameSite=Lax, andSecurein production or behind HTTPS. A separate CSRF cookie pairs with theX-CSRF-Tokenheader for cookie-authenticated mutations. - CSRF — State-changing API requests authenticated via session cookie require a matching CSRF token. The SPA sends the token automatically.
- CORS — Cross-origin access is denied unless the origin is listed in
CORS_ALLOWED_ORIGINS(no wildcard reflection). - Rate limiting — Login endpoints are limited per client IP (
AUTH_RATE_LIMIT,AUTH_RATE_LIMIT_WINDOW). - Request limits — Body size capped via
MAX_REQUEST_BODY_SIZE; server read/write/idle timeouts viaHTTP_*_TIMEOUT. - Payload compression — Bodies larger than 32 KiB are compressed (responses: brotli then gzip when negotiated via
HTTP_RESPONSE_COMPRESSION; requests: brotli with gzip fallback). Smaller bodies stay uncompressed. - RBAC & audit — All
/api/*routes except health/auth config require authentication. Mutations are audit-logged. Cluster tokens/creds are never returned in API JSON. - Production — Set
ENV=production,ENCRYPTION_KEY,SESSION_PRIVATE_KEY/SESSION_PUBLIC_KEY, and a strongADMIN_PASSWORD. The server refuses to start if these are missing or weak.
Run make test-security for automated checks (headers, cookies, CSRF, rate limits, RBAC, secret leakage).
See api/swagger.yaml (regenerate with make openapi) or live spec at GET /api/openapi.yaml.
Key endpoints:
GET /api/health— readiness (postgres + default NATS cluster)GET /api/v1/clusters/{id}/metrics/history— JetStream/server metric history (Postgres snapshots)GET /api/v1/auth/config—{ auth_enabled, basic_enabled }POST /api/v1/auth/login— session loginGET /api/v1/auth/me— current user profilePOST /api/v1/auth/logout— clear session cookieGET /api/v1/audit— audit log (admin)GET /api/v1/users— user list (admin)GET /api/v1/clusters— list registered clusters (devops-configured)- Cluster-scoped JetStream, KV, Object Store, and live WebSocket paths under
/api/v1/clusters/{id}/…
helm upgrade --install nats-consol ./deploy/helm/nats-consol \
--set secrets.databaseUrl='postgres://…' \
--set secrets.encryptionKey='your-32-char-key'Tests are grouped by intent, not by framework. One shared tests/testutil package backs integration, contract, security, and database suites (testcontainers + in-memory HTTP).
| Category | Command | Docker / stack required |
|---|---|---|
| Unit | make test-unit |
No |
| Integration (API + NATS + DB) | make test-integration |
Yes (testcontainers) |
| Database | included in make test-integration |
Yes |
| Contract (camelCase JSON vs frontend) | make test-contract |
Yes |
| Security (auth, RBAC, headers, CSRF, rate limits, no secrets in responses) | make test-security |
Yes |
| Regression (CI gate) | make test-regression |
Yes |
| Web unit + Playwright e2e | make test-web |
No (mocked API e2e) |
| Smoke / E2E / Acceptance | make test-smoke |
Yes (docker compose up) |
| Load / Throughput / Performance | make test-performance |
Yes + vegeta |
| Stress | make test-stress |
Yes + vegeta (higher rate) |
Quick start:
make test-unit # fast, no Docker
make test-regression # integration + contract + security
docker compose up --build -d && make test-smoke
docker compose up -d && make test-performance # needs vegeta installed
docker compose up -d && make test-stress # needs vegeta; higher RPSSet SKIP_TESTCONTAINERS=1 to skip Docker-backed Go tests.
Environment variables for smoke/performance/stress scripts:
| Variable | Default | Description |
|---|---|---|
BASE_URL |
http://localhost:8080 |
Running console URL |
AUTH |
admin:admin |
Basic auth for smoke/perf/stress |
TLS_INSECURE |
— | Set 1 to skip TLS verify when BASE_URL is HTTPS with a lab cert |
PERF_MIN_RPS |
load: 10 / stress: 50 |
Minimum throughput |
PERF_MAX_P99_MS |
load: 2000 / stress: 5000 |
Max p99 latency (ms) |
CI (.github/workflows/test.yml) on every pull request to main: Go lint/tests/build, web lint/typecheck/build/Playwright e2e, regression (integration/contract/security), race detector (live WebSocket), and compose smoke (:8080). The All checks passed job must succeed before merge. Performance and stress baselines run on pushes to main only (advisory, not merge-blocking).
Post-deploy UI checks: see docs/manual-test-checklist.md.
- OpenAPI-generated CLI
- Account/JWT operator key generation UI
Apache License 2.0 — see LICENSE.