Files
summercms/modules/lighthouse/README.md
Jakub Zych cada7a4442 feat(11-03): add the lighthouse realtime package and its Centrifugo driver
- lighthouse: Service/From with realtime.driver selection, RegisterDriver
  registry, null/log/memory drivers, Route/Surface/Mount, users and actors
- centrifugo: HTTP API client (apikey header, 2xx success, no request
  without a key), five-generator HS256 TokenIssuer, TokenHandler with the
  WinterCMS 401/503 bodies
- module README and root modules table row
2026-09-30 12:18:11 +02:00

9.6 KiB

lighthouse

Transport-neutral realtime: a publisher interface with pluggable drivers, subscribe-time channel authorization, and model broadcasts enqueued in the write transaction.

import "git.golem15.com/golem15/summercms/modules/lighthouse"

import _ "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"

Overview

lighthouse is the SummerCMS counterpart of the WinterCMS websockets plugin. The package itself knows no transport. It owns the interfaces application code writes against, and a driver package supplies the transport. A driver registers itself from its init function, the way database/sql drivers do, and the application picks one with realtime.driver.

lighthouse.From builds the app-scoped lighthouse.Service on first use and publishes it on the app. The service holds the selected driver, the application's user lookup, and the broadcast settings.

A driver may need HTTP endpoints, such as a token route for signed-in users or a callback the realtime server calls. It declares them as lighthouse.Route values, each tagged with a lighthouse.Surface. The application mounts them once with lighthouse.Mount and decides the guard, group and rate-limit bucket per surface. Switching drivers never edits the application's route file.

The centrifugo sub-package is the Centrifugo driver. It has a hand-rolled net/http client for the Centrifugo HTTP API, a token issuer with the claims of the WinterCMS JwtTokenGenerator, and the token route handler.

Features

  • Driver selection by realtime.driver. The built-in drivers are null (the default; it discards everything), log (logs channel names and the event, never the payload) and memory (lighthouse.MemoryDriver records every lighthouse.Publication for tests). An unknown name is a boot error that lists the registered drivers.
  • Third-party drivers: lighthouse.RegisterDriver with a lighthouse.DriverFactory. A duplicate name panics at init.
  • The lighthouse.Publisher interface (Publish for one channel, Broadcast for several) and the lighthouse.Driver interface, which adds Name and Routes.
  • Route mounting by surface: lighthouse.UserAuth, lighthouse.ServerToServer and lighthouse.Public. lighthouse.Mount puts user and public routes in Group and server-to-server routes in GroupRaw, each with the surface middleware followed by lighthouse.Surfaces.Middleware. A lighthouse.UserAuth route with no user middleware is refused, so a token route can never be mounted without a guard. Every route is validated before any is registered.
  • Users and actors: the application installs a lighthouse.UserLookup with lighthouse.Service.SetUserLookup. lighthouse.Service.User loads a lighthouse.User (id and display name). lighthouse.Service.Actor returns the lighthouse.Actor of a request, and lighthouse.SystemActor when there is no signed-in user or the principal is a backend admin.
  • Centrifugo driver (centrifugo.Driver, driver name centrifugo):
    • centrifugo.Client POSTs publish, broadcast, presence and unsubscribe calls with Authorization: apikey <key> and a 5 s timeout. The publish body is {"channel":…,"data":{"event":…,"payload":…,"timestamp":"…+00:00"}}, with an empty payload sent as []. Any 2xx status counts as success. With an empty API key nothing is sent and the call returns centrifugo.ErrNotConfigured. The key never appears in logs or errors.
    • centrifugo.TokenIssuer signs HS256 tokens with five generators, the same as the WinterCMS generator: ForUser (claims sub, exp, info with only name), Subscription, Anonymous (sub "" and a 5-minute lifetime), ForIdentifier (an empty info is encoded as []) and SubscriptionForIdentifier. It refuses to sign with an empty secret.
    • centrifugo.TokenHandler serves the token route. It answers 401 {"error":"Unauthorized"} when no user is signed in, 503 {"error":"WebSocket not configured"} when the token secret is empty, and otherwise 200 {"token":"…"}. It sends Cache-Control: no-cache, private and no trailing newline.

Usage

An application selects the driver in config/realtime.yaml:

driver: centrifugo
centrifugo:
  token_secret: ""   # set with SUMMER_REALTIME__CENTRIFUGO__TOKEN_SECRET

A plugin imports the driver package for its side effect, builds the service at Boot and installs a user lookup:

package acme

import (
	"context"

	"git.golem15.com/golem15/summercms/modules/backpack"
	"git.golem15.com/golem15/summercms/modules/lighthouse"
	_ "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
)

func (p *Plugin) Boot(app *backpack.App) error {
	svc, err := lighthouse.From(app)
	if err != nil {
		return err
	}
	p.realtime = svc
	svc.SetUserLookup(func(ctx context.Context, id uint) (lighthouse.User, bool, error) {
		return lookupAcmeUser(ctx, id) // the application's own user model
	})
	return nil
}

and mounts the driver's routes once:

func (p *Plugin) Routes(r pact.Router) error {
	return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{
		UserAuth:       surf.Use("jwt.auth"),
		ServerToServer: surf.Use(),
		Middleware:     surf.Use("throttle:acme-realtime"),
	})
}

Tests select the memory driver and read what was published:

mem := svc.Driver().(*lighthouse.MemoryDriver)
for _, pub := range mem.Publications() {
	fmt.Println(pub.Method, pub.Channels, pub.Event)
}

API reference

lighthouse

Identifier Description
lighthouse.From(app) The app's *lighthouse.Service, built and published on first use.
lighthouse.Service The realtime service: Driver, Logger, Namespace, Queue, Timeout, SetUserLookup, User, Actor.
lighthouse.Publisher Publish(ctx, channel, event, payload) and Broadcast(ctx, channels, event, payload).
lighthouse.Driver lighthouse.Publisher plus Name() and Routes().
lighthouse.DriverFactory func(app, svc) (lighthouse.Driver, error).
lighthouse.RegisterDriver(name, factory) Registers a driver from an init function.
lighthouse.MemoryDriver, lighthouse.NewMemoryDriver, lighthouse.Publication The recording driver and its records (Method, Channels, Event, Payload, Timestamp).
lighthouse.Route Name, Method, Path, Surface, Handler.
lighthouse.Surface, lighthouse.UserAuth, lighthouse.ServerToServer, lighthouse.Public Who calls a route.
lighthouse.Surfaces Application middleware per surface plus Middleware for every route.
lighthouse.Mount(r, driver, surfaces) Registers a driver's routes.
lighthouse.User, lighthouse.UserLookup A user id with a display name, and the application's lookup.
lighthouse.Actor, lighthouse.SystemActor Who caused a broadcast: {"user_id":…,"name":…}.
lighthouse.DurationSetting(cfg, path) Reads a duration string or an integer number of seconds.
lighthouse.DefaultDriver, lighthouse.DefaultQueue, lighthouse.DefaultTimeout Defaults of realtime.driver, realtime.broadcast_queue and realtime.broadcast_timeout.

lighthouse/centrifugo

Identifier Description
centrifugo.Config, centrifugo.LoadConfig The realtime.centrifugo.* settings with their defaults.
centrifugo.Client, centrifugo.NewClient HTTP API client: Publish, Broadcast, Presence, Unsubscribe, Enabled, DebugInfo.
centrifugo.DebugInfo api_url, enabled, api_key_set.
centrifugo.TokenIssuer, centrifugo.NewTokenIssuer HS256 token generators: ForUser, Subscription, Anonymous, ForIdentifier, SubscriptionForIdentifier, Configured.
centrifugo.TokenHandler(svc, issuer) The token route handler.
centrifugo.Driver, centrifugo.NewDriver, centrifugo.DriverName The lighthouse.Driver, with Client, Issuer and Config accessors.
centrifugo.ErrNotConfigured Returned when the API key or token secret an operation needs is empty.

Configuration

Key Default Description
realtime.driver null null, log, memory, or a registered driver such as centrifugo.
realtime.broadcast_namespace "" Prefix applied to broadcast channel names.
realtime.broadcast_queue broadcasts River queue of broadcast jobs.
realtime.broadcast_timeout 5 Broadcast job timeout, in seconds or as a duration string.
realtime.centrifugo.api_url http://127.0.0.1:8001/api Centrifugo HTTP API base.
realtime.centrifugo.api_key "" HTTP API key; empty disables publishing.
realtime.centrifugo.token_secret "" HS256 token secret; empty makes the token route answer 503.
realtime.centrifugo.token_ttl 3600 Token lifetime, in seconds or as a duration string.
realtime.centrifugo.ws_url /ws WebSocket URL of the Centrifugo server.
realtime.centrifugo.proxy_secret "" Expected X-Centrifugo-Secret of subscribe proxy calls.
realtime.centrifugo.token_path /api/realtime/token Path of the token route.
realtime.centrifugo.subscribe_path /api/realtime/subscribe Path of the subscribe proxy route.

Dependencies

  • backpack, bouncer, compass, pact and wire from this repository.
  • github.com/golang-jwt/jwt/v5 (centrifugo token signing).
  • The Centrifugo client is plain net/http; no Centrifugo SDK is used.

Testing

go test ./modules/lighthouse/...

A test selects realtime.driver: memory and reads lighthouse.MemoryDriver.Publications, or points realtime.centrifugo.api_url at an httptest server to see the exact Centrifugo requests.