SSE & Datastar

Gastro provides a lightweight SSE helper for streaming events from the server to the browser, enabling real-time UI updates with Datastar and HTMX.

How It Works

  1. A Gastro page renders the initial HTML (as usual)
  2. Client-side attributes open an SSE connection to an API endpoint
  3. Your Go handler writes SSE events that patch the DOM

SSE endpoints are plain Go HTTP handlers — no compiler changes needed. Register them alongside Gastro routes in your main.go.

Generic SSE

The core SSE helper in pkg/gastro is framework-agnostic. It works with any client that consumes text/event-stream:

func handleUpdates(w http.ResponseWriter, r *http.Request) {
    sse := gastro.NewSSE(w, r)

    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-sse.Context().Done():
            return
        case <-ticker.C:
            now := time.Now().Format("15:04:05")
            sse.Send("time", now)
        }
    }
}

Methods available on the SSE helper:

Datastar Integration

The pkg/gastro/datastar subpackage formats events using Datastar's SSE protocol:

var count atomic.Int64

func handleIncrement(w http.ResponseWriter, r *http.Request) {
    n := count.Add(1)

    html, err := gastro.Render.Counter(
        gastro.CounterProps{Count: int(n)},
    )
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }

    sse := datastar.NewSSE(w, r)
    sse.PatchElements(html)
}

Datastar Page

Add Datastar attributes to your .gastro pages to trigger SSE connections:

---
import Layout "components/layout.gastro"
Title := "Counter"
---
{{ wrap Layout (dict "Title" .Title) }}
    <div id="count">0</div>
    <button data-on:click="@get('/api/increment')">+1</button>
{{ end }}

Patch Options

Datastar supports selectors, patch modes, and signal patching:

sse.PatchElements(html,
    datastar.WithSelector("#dashboard"),
    datastar.WithMode(datastar.ModeInner),
)

sse.PatchSignals(map[string]any{
    "count": 42, "loading": false,
})

sse.RemoveElement("#toast-1")

Wiring It Up

Create a top-level http.ServeMux and mount both API routes and Gastro page routes:

func main() {
    mux := http.NewServeMux()

    // API/SSE endpoints first
    mux.HandleFunc("GET /api/increment", handleIncrement)
    mux.HandleFunc("GET /api/clock", handleClock)

    // Gastro page routes (catch-all)
    mux.Handle("/", gastro.Routes())

    http.ListenAndServe(":4242", mux)
}

Type-Safe Rendering

The compiler generates a Render API for calling Gastro components from SSE handlers with full type safety:

// Each component gets a typed Render method
html, err := gastro.Render.Counter(
    gastro.CounterProps{Count: 42},
)

// Components with slots accept optional children
inner, _ := gastro.Render.Counter(
    gastro.CounterProps{Count: 42},
)
full, _ := gastro.Render.Layout(
    gastro.LayoutProps{Title: "Dashboard"},
    template.HTML(inner),
)
WhatSafety
Method nameCompile-time — method exists or doesn't
Props fieldsCompile-time — struct fields checked by Go
Props typesCompile-time — Go type system

Design Notes

Try the live SSE demo →