NATS
NATS is used as the internal pub/sub message bus. Today it's used only within CoreAPI itself (published and subscribed by the same process), but the client wrapper and infrastructure are already positioned for cross-service use later — e.g. other apps subscribing to media/channel/schedule events without going through CoreAPI's HTTP API.
Current status: Active, but narrow. Events are published and logged; nothing yet consumes them to drive real behavior (no websocket push to frontends, no cross-service side effects). See Current Usage below for specifics.
Compose Service
services:
nats:
image: nats:alpine
container_name: nats
ports:
- "4222:4222" # client connections
- "8222:8222" # HTTP monitoring
networks:
- internal
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8222/healthz"]
interval: 5s
timeout: 3s
retries: 5
| Port | Purpose |
|---|---|
4222 |
Client connections — this is what natsclient.Init() connects to |
8222 |
HTTP monitoring endpoint — used by the healthcheck (/healthz), also browsable directly for connection/subscription stats |
Environment Variables
| Variable | Value | Role | Status |
|---|---|---|---|
NATS_URL |
nats://nats:4222 |
Client connection string. Falls back to this same value in code if unset — see natsclient.Init(). |
Active |
That's the only NATS-specific env var in the system today. No auth, no TLS, no clustering config — this is a minimal single-node setup appropriate for local/dev. Production hardening (auth tokens, TLS, clustering) isn't configured yet and would need to be added before any external-facing or multi-tenant use.
Client Wrapper
CoreAPI wraps nats.go in a small package: pkg/natsclient.
package natsclient
var NC *nats.Conn
func Init() // connects, called once in main.go
func Publish(subject string, data interface{}) error // JSON-marshals data, publishes
func Subscribe(subject string, handler func([]byte)) (*nats.Subscription, error)
func Close() // called via defer in main.go
Wiring in main.go
natsclient.Init()
defer natsclient.Close()
Initialized once at startup, alongside Postgres, Redis, SeaweedFS, and AuthAPI — treated as a core infra dependency, not optional.
How to publish
import "mashup.castis.io/playtelly/CoreAPI/pkg/natsclient"
natsclient.Publish("my.event.name", map[string]interface{}{
"id": 123,
"status": "done",
})
Publish JSON-marshals whatever you pass as data — no schema enforcement,
so subject naming and payload shape are a convention, not a contract. Keep
payloads consistent per-subject by hand.
How to subscribe
natsclient.Subscribe("my.event.name", func(data []byte) {
var e MyEventPayload
json.Unmarshal(data, &e)
// handle it
})
Typically registered once at startup via a RegisterSubscribers() function
per domain (see media domain's handler_nats.go for the existing pattern).
Current Usage (CoreAPI)
The media domain is the only current publisher/subscriber. Three events are published during the upload-processing lifecycle:
| Subject | Published when | Subscribed — currently does |
|---|---|---|
media.processing.started |
POST /media/:id/process called, or tusd webhook fires |
Logs only |
media.ready |
Filer move to final path completes, status → ready |
Logs only |
media.thumbnail.ready |
Thumbnail generated and uploaded to SeaweedFS | Logs only |
Both the publish and subscribe sides live in the same CoreAPI process today
— there's no cross-service consumption yet. The subscriber handlers
(domain/media/handlers/handler_nats.go) have explicit // future: comments
marking intended next steps:
// future: notify frontend via WebSocket bridge
// future: provisioning.UnlockEntitlement(e.TenantID, e.MediaID)
// future: analytics.RecordUpload(e.TenantID, e.MediaID)
Practical implication: MediaList.jsx's live-update behavior (detecting
when a processing item becomes ready) is driven by polling
(setInterval, every 3–4s), not by NATS. NATS events fire correctly and are
logged server-side, but nothing today pushes them to the frontend. If
real-time push ever replaces polling, this is the mechanism it would build
on top of — the publish side already exists.
Verifying NATS is running
# health check
curl http://localhost:8222/healthz
# connection/subscription stats
curl http://localhost:8222/connz
curl http://localhost:8222/subsz
subsz is useful for confirming a subscriber actually registered — if
handler_nats.go's RegisterSubscribers() didn't get called for some
reason, you'd see zero subscriptions for the media.* subjects here even
though publishing wouldn't error (NATS doesn't require a subscriber to
exist for Publish to succeed — messages with no subscribers are simply
dropped, no error, no queue, no retry).
Design notes for future cross-service use
A few things worth deciding before other apps start subscribing, since none of this is enforced today:
- Subject naming convention — currently
media.<event>, dot-delimited, domain-prefixed. Worth keeping consistent (distribution.<event>,playback.<event>, etc.) if other domains start publishing. - No delivery guarantees — this is core NATS (fire-and-forget), not JetStream. If a subscriber is down when an event publishes, that event is lost — no replay, no persistence. Fine for today's "log it" use case; would need JetStream (or a different subject/durable-consumer setup) if a future consumer needs guaranteed delivery (e.g. billing, entitlement unlocking).
- No schema/versioning — payloads are ad-hoc
map[string]interface{}or anonymous structs per publish call. If cross-service consumers appear, worth formalizing payload shapes (shared Go structs, or a schema doc) so a publisher-side field rename doesn't silently break a consumer in a different repo.