Build a small sessionstream app by registering typed schemas, installing a hub, publishing events, and reading a snapshot.
This guide walks through the smallest useful sessionstream application. The goal is not to build a production service. The goal is to see the shape of the framework: commands enter a session, handlers publish backend events, projections derive UI and timeline state, and snapshots make the result recoverable.
The runnable reference is examples/chatdemo. Use that example when you want complete code with generated protobuf messages and tests.
You will wire a small chat-style service that accepts a prompt and produces timeline state. The moving pieces are:
SchemaRegistry with concrete protobuf payloads.Hub configured with that registry.Every command, backend event, UI event, and timeline entity has a name and a concrete protobuf payload type. The schema registry stores those prototypes so the framework can validate, marshal, unmarshal, and hydrate payloads consistently.
reg := sessionstream.NewSchemaRegistry()
if err := chatdemo.RegisterSchemas(reg); err != nil {
return err
}
The reference implementation registers messages like this:
reg.RegisterCommand(chatdemo.CommandStartInference, &chatdemov1.StartInferenceCommand{})
reg.RegisterEvent(chatdemo.EventInferenceStarted, &chatdemov1.InferenceStartedEvent{})
reg.RegisterUIEvent(chatdemo.UIMessageStarted, &chatdemov1.ChatMessageUpdate{})
reg.RegisterTimelineEntity(chatdemo.TimelineEntityChatMessage, &chatdemov1.ChatMessageEntity{})
Use concrete protobuf messages. Do not register top-level *structpb.Struct; sessionstream-lint rejects those registrations.
The Hub is the routing center. It receives commands, looks up handlers, gives handlers an event publisher, runs projections, applies timeline entities, and fans out UI events.
hub, err := sessionstream.NewHub(
sessionstream.WithSchemaRegistry(reg),
)
if err != nil {
return err
}
The default hub uses an in-memory/no-op hydration store. Production or restart-sensitive applications should install a real store such as the SQLite hydration implementation.
Handlers and projections are application code. sessionstream does not know what a chat message, inventory widget, or workflow step means. It only knows how to route and apply the events those features publish.
engine := chatdemo.NewEngine()
if err := chatdemo.Install(hub, engine); err != nil {
return err
}
Inside the installer, the application registers command handlers and projections:
hub.RegisterCommand(chatdemo.CommandStartInference, engine.handleStartInference)
hub.RegisterUIProjection(sessionstream.UIProjectionFunc(uiProjection))
hub.RegisterTimelineProjection(sessionstream.TimelineProjectionFunc(timelineProjection))
A handler publishes events instead of returning UI state:
return pub.Publish(ctx, sessionstream.Event{
Name: chatdemo.EventUserMessageAccepted,
SessionId: cmd.SessionId,
Payload: &chatdemov1.UserMessageAcceptedEvent{
MessageId: userMessageID,
Role: "user",
Content: prompt,
},
})
That choice is the core design. Events are the canonical history. UI and timeline state are projections of that history.
Commands always belong to a session. The same command name can be submitted to different sessions without sharing timeline state.
service, err := chatdemo.NewService(hub, engine)
if err != nil {
return err
}
if err := service.SubmitPrompt(ctx, "session-1", "Explain ordinals"); err != nil {
return err
}
The command path is:
Submit -> Hub -> CommandHandler -> EventPublisher -> backend Event -> projections -> UIEvent + TimelineEntity
A snapshot returns the current hydrated timeline state for a session.
snapshot, err := service.Snapshot(ctx, "session-1")
if err != nil {
return err
}
Snapshots are what make reconnects work. A websocket client receives the current snapshot first and then future live UI events.
From the Sessionstream repository:
go test ./examples/chatdemo -count=1
make test
If you are changing schema registrations, also run:
make schema-vet
| Problem | Cause | Solution |
|---|---|---|
unknown command | The command name was not registered with the hub. | Call hub.RegisterCommand during installation. |
payload type mismatch | The submitted payload does not match the schema registry prototype. | Submit the generated protobuf type registered for that command. |
| No UI events appear | No UI projection is registered, or the projection ignores the backend event name. | Register UIProjectionFunc and handle the event name explicitly. |
| Snapshot is empty | No timeline projection emitted entities, or the store is no-op for your scenario. | Register a timeline projection and use a hydration store appropriate for your app. |
sessionstream-lint rejects *structpb.Struct | A top-level schema registration is generic. | Define a concrete protobuf message and register that type. |
sessionstream-user-guide for the broader mental model.sessionstream-reference for API and package responsibilities.sessionstream-schema-vet-playbook for schema-vet usage.examples/chatdemo/chat.go for a complete small application.