Files
via/internal/examples/nats-chatroom
Ryan Hamamura a7ace9099f feat: replace log with rs/zerolog for structured logging
Switch from the standard library log package to rs/zerolog with
ConsoleWriter for colorful terminal output in dev mode and JSON
output in production. Users can now provide their own logger via
Options.Logger or set the level via Options.LogLevel.
2026-01-31 08:18:24 -10:00
..

NATS Chatroom Example (Embedded)

A chatroom built with Via and an embedded NATS server, demonstrating pub/sub messaging as an alternative to the custom Rooms implementation in ../chatroom.

Uses delaneyj/toolbelt/embeddednats to run NATS inside the same binary - no external server required.

Key Differences from Original Chatroom

Aspect Original (../chatroom) This Example
Pub/sub Custom Rooms struct (~160 lines) NATS subjects
Member tracking Manual map[TU]Syncable NATS handles subscribers
Publish timing Ticker every 100ms + dirty flag Instant delivery
Durability None (in-memory) JetStream persists to disk
Multi-instance Not supported Works across server instances
External deps None None (NATS embedded in binary)

Run the Example

go run ./internal/examples/nats-chatroom

That's it. No separate NATS server needed.

Open multiple browser tabs at http://localhost:7331 to see messages broadcast across all clients.

How Embedded NATS Works

// Start embedded NATS server (JetStream enabled by default)
ns, err := embeddednats.New(ctx,
    embeddednats.WithDirectory("./data/nats"),
)
ns.WaitForServer()

// Get client connection to embedded server
nc, err := ns.Client()

Data is persisted to ./data/nats/ for JetStream durability.

Architecture

┌─────────────────────────────────────────────────────────┐
│                    Single Binary                         │
│                                                          │
│  Browser A          Embedded NATS         Browser B      │
│      │                   │                    │          │
│      │-- Via Action ---> │                    │          │
│      │   (Send msg)      │                    │          │
│      │                   │                    │          │
│      │              nc.Publish()              │          │
│      │              "chat.room.Go"            │          │
│      │                   │                    │          │
│      │<-- Subscribe -----|---- Subscribe --->│          │
│      │    callback       │    callback        │          │
│      │                   │                    │          │
│      │-- c.Sync() ------>│<--- c.Sync() -----|          │
│      │   (SSE)           │     (SSE)          │          │
│                                                          │
└─────────────────────────────────────────────────────────┘

JetStream Durability

Messages persist to disk via JetStream:

js.AddStream(&nats.StreamConfig{
    Name:      "CHAT",
    Subjects:  []string{"chat.>"},
    MaxMsgs:   1000,  // Keep last 1000 messages
    MaxAge:    24 * time.Hour,
})

Stop and restart the app - chat history survives.

Code Comparison

Original chatroom - 160+ lines of custom pub/sub:

  • Rooms struct with named rooms
  • Room with member tracking, mutex, dirty flag
  • Ticker-based publish loop
  • Manual join/leave channels

This example - ~60 lines of NATS integration:

  • embeddednats.New() starts the server
  • nc.Subscribe(subject, handler) for receiving
  • nc.Publish(subject, data) for sending
  • NATS handles delivery, no polling

Next Steps

If this pattern proves useful, it could be promoted to a Via plugin:

// Hypothetical future API
v.Config(via.WithEmbeddedNATS("./data/nats"))

// In page init
c.Subscribe("events.user.*", func(data []byte) {
    c.Sync()
})

c.Publish("events.user.login", userData)