Getting Started with Sessionstream

Build a small sessionstream app by registering typed schemas, installing a hub, publishing events, and reading a snapshot.

Sections

Terminology & Glossary
πŸ“– Documentation
Navigation
4 sectionsv0.1
πŸ“„ Getting Started with Sessionstream β€” glaze help sessionstream-getting-started
sessionstream-getting-started

Getting Started with Sessionstream

Build a small sessionstream app by registering typed schemas, installing a hub, publishing events, and reading a snapshot.

Tutorialsessionstreamgetting-startedcommandsprojectionshydration

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.

What you will build

You will wire a small chat-style service that accepts a prompt and produces timeline state. The moving pieces are:

  1. A SchemaRegistry with concrete protobuf payloads.
  2. A Hub configured with that registry.
  3. Command handlers that publish backend events.
  4. UI and timeline projections.
  5. A snapshot call that reads hydrated state.

Step 1: Register payload schemas

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.

Step 2: Create the hub

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.

Step 3: Install handlers and projections

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.

Step 4: Submit a command

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

Step 5: Read a snapshot

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.

Complete local validation

From the Sessionstream repository:

go test ./examples/chatdemo -count=1
make test

If you are changing schema registrations, also run:

make schema-vet

Troubleshooting

ProblemCauseSolution
unknown commandThe command name was not registered with the hub.Call hub.RegisterCommand during installation.
payload type mismatchThe submitted payload does not match the schema registry prototype.Submit the generated protobuf type registered for that command.
No UI events appearNo UI projection is registered, or the projection ignores the backend event name.Register UIProjectionFunc and handle the event name explicitly.
Snapshot is emptyNo 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.StructA top-level schema registration is generic.Define a concrete protobuf message and register that type.

See Also

  • 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.