Self-hosted realtime push service (Pusher-style). Browsers hold a WebSocket and subscribe to channels; backends POST to push a message into a channel. Horizontally scalable, backed by NATS — no Redis.
- slave — holds the WebSocket connections, verifies the JWT locally (RSA public key), and fans out messages to subscribers.
- master — a stateless REST API to push messages and query presence.
- NATS — carries messages between nodes (bus) and answers presence queries (request/reply). No Redis, no token store, no decode service.
client ──ws──▶ slave ──subscribe──▶ NATS ◀──publish── master ◀──POST── backend
Both roles are stateless and scale horizontally. Full write-up (bus, presence, evolution from Redis) in docs/ARCHITECTURE.md.
- NATS (the only backend) —
nats-server2.10+. - An RSA key pair — master/slave verify the JWT with the public key; your own auth service signs JWTs with the private key. See docs/KEYS.md for the full generate → sign → verify → rotate walkthrough.
The stack ships a demo RSA key (docker-compose/public.pem, matched by
test/key/private.pem), so it runs out of the box — no setup:
docker compose -f docker-compose/docker-compose.yml up --buildBrings up nats + gusher-master (:7777) + gusher-slave (:8888), no Redis.
See it work — one command brings the stack up, signs a JWT, subscribes over WebSocket, pushes a message and asserts it arrives, then tears down:
make smokeOr test by hand against the running stack — sign a token with the demo key, then auth → connect → push:
# 1. sign a JWT (claims: app_key TEST, channels AA/BB)
go run test/jwtgenerate/jwtgenerate.go gen --private-key test/key/private.pem
# 2. exchange it for a session token
curl -s localhost:8888/v1/auth -d '{"jwt":"<JWT>"}'
# 3. open the socket: ws://localhost:8888/v1/apps/TEST/ws?token=<token>
# then subscribe: {"event":"gusher.subscribe","data":{"channel":"AA"}}
# 4. push from any backend — the subscribed socket receives it
curl -s localhost:7777/v1/apps/TEST/channels/AA/messages -d '{"event":"EVENT","data":{"hi":"there"}}'Use your own keys (for real deployments) — generate an RSA pair and drop the
public half next to the compose file (or point GUSHER_PUBLIC_PEM_FILE at it):
make rsakey # writes private.pem + public.pemFull key lifecycle (generate → sign → verify → rotate) in docs/KEYS.md.
go build -ldflags "-X main.name=gusher" -o gusher.cluster .
# slave
GUSHER_NATS_ADDR=nats://127.0.0.1:4222 GUSHER_PUBLIC_PEM_FILE=./public.pem \
GUSHER_API_LISTEN=:8888 ./gusher.cluster slave
# master
GUSHER_NATS_ADDR=nats://127.0.0.1:4222 GUSHER_PUBLIC_PEM_FILE=./public.pem \
GUSHER_MASTER_API_LISTEN=:7777 ./gusher.cluster masterSee slave.env.example / master.env.example for the full env list.
The repo's docker-compose.yml and example/ build from source (for dev /
the demo). To deploy without building, pull the published image from Docker Hub —
syhlion/gusher.cluster,
tagged per release (:3.0.0 / :3 / :latest):
docker pull syhlion/gusher.cluster:latest
# the image takes the role as its command; give it a reachable NATS + your public key:
docker run --rm -p 7777:7777 \
-e GUSHER_NATS_ADDR=nats://your-nats:4222 \
-e GUSHER_MASTER_API_LISTEN=:7777 \
-e GUSHER_PUBLIC_PEM_FILE=/public.pem \
-v "$PWD/public.pem:/public.pem:ro" \
syhlion/gusher.cluster:latest masterTo pull instead of build in your own compose, swap the build: block for
image: syhlion/gusher.cluster:<tag>.
POST /v1/authwith{"jwt":"<JWT>"}→{"token":"<JWT>"}(the JWT is verified locally and returned as the token — stateless, no store).GET /v1/apps/{app}/ws?token=<token>→ WebSocket. Subscribe with{"event":"gusher.subscribe","data":{"channel":"AA"}}.- Backend pushes:
POST /v1/apps/{app}/channels/{channel}/messageswith{"event":"...","data":...}.
The JWT carries the gusher claim — {"app_key","user_id","channels"} — signed
RS256. See doc/protocal.md for the full WebSocket
protocol and doc/api.md for the REST API.
- Health:
GET /healthz(liveness) ·GET /readyz(readiness — 200 only while NATS is connected). - Console / stats: the master serves a single-page console at
GET /ui(global connections/users + per-app channels) overGET /v1/statsandGET /v1/apps. - NATS auth: set
GUSHER_NATS_CREDS=/path/to/app.credsfor user credentials; use atls://address (or NATS server config) for TLS. The client auto-reconnects.
Output is selectable and rotated, via env:
| Env | Values |
|---|---|
GUSHER_LOG_OUTPUT |
stdout (default) / file / both |
GUSHER_LOG_FILE |
path (for file / both) |
GUSHER_LOG_FORMAT |
json (default) / text |
GUSHER_LOG_MAX_SIZE_MB / _MAX_BACKUPS / _MAX_AGE_DAYS / _COMPRESS |
rotation |
- example/ — runnable demo: type on a backend, watch it arrive live on a frontend (one
docker composecommand) - docs/ARCHITECTURE.md — architecture, NATS bus/presence, evolution from Redis (with diagrams)
- doc/protocal.md — WebSocket protocol
- doc/api.md — REST API
- docs/KEYS.md — RSA keys: generate → sign → verify → rotate
- docs/LOAD-TEST.md — load testing
go test ./... # unit + e2e (e2e spins up an in-process NATS, no external deps)
