Go Library API Reference

Scope: This reference documents the livetemplate Go library (github.com/livetemplate/livetemplate). For the CLI tool, see the lvt repository.

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/livetemplate/livetemplate"
)

type AppController struct {
    // Dependencies (singleton, never cloned)
}

type AppState struct {
    Count int
}

func (c *AppController) Increment(state AppState, ctx *livetemplate.Context) (AppState, error) {
    state.Count++
    return state, nil
}

func main() {
    tmpl, err := livetemplate.New("app")
    if err != nil {
        log.Fatal(err)
    }
    tmpl.Parse(`<h1>Count: {{.Count}}</h1><button name="increment">+</button>`)

    handler := tmpl.Handle(&AppController{}, livetemplate.AsState(&AppState{}))
    http.Handle("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Template

The Template type manages template parsing, execution, and tree-based update generation.

Creating Templates

func New(name string, opts ...Option) (*Template, error)

Creates a new template with the given name and options. Auto-discovers template files in the caller's directory.

tmpl, err := livetemplate.New("todos",
    livetemplate.WithDevMode(true),
    livetemplate.WithSessionStore(redisStore),
)

Parsing

func (t *Template) Parse(text string) (*Template, error)

Parses a template string. Supports {{define}}, {{block}}, and {{template}} composition via automatic flattening.

func (t *Template) ParseFiles(filenames ...string) (*Template, error)

Parses template files. The first file is the main template; additional files provide definitions.

func (t *Template) ParseGlob(pattern string) (*Template, error)

Parses all template files matching the glob pattern.

func (t *Template) Funcs(funcMap template.FuncMap) *Template

Registers custom template functions. Must be called before Parse or ParseFiles.

Mounting

func (t *Template) Handle(controller interface{}, state State, opts ...HandleOption) LiveHandler

Creates an HTTP/WebSocket handler from a controller and initial state. The controller is a singleton holding dependencies; state is cloned per session.

handler := tmpl.Handle(&TodoController{DB: db}, livetemplate.AsState(&TodoState{}))

HandleOption:

Option Description
WithEphemeralSweepTTL(ttl time.Duration) How long an idle ephemeral HTTP cache entry survives before eviction (default 30m; no effect in persistent mode)

The session store is set at construction via WithSessionStore (a New option), not per-handler.


Validate

func Validate(templateText string, opts ...ValidateOption) ([]Diagnostic, error)

Parses templateText the way the live renderer does — against the framework's real function set and any supplied component templates — and returns the problems found. An empty slice means the template parses cleanly and will be served rather than silently dropped.

Why this exists: Execute and ExecuteUpdates catch a first-render failure and fall back to an HTML-structure tree. A malformed template — an unclosed {{range}}, an unknown function, an unresolved {{template}} — therefore renders degraded and returns no error. A tool that wants to reject a bad template before serving it had nothing to call. Validate is that call.

type Diagnostic struct {
    Line     int      // 1-based line in the supplied text; 0 if the parser reported none
    Severity Severity // SeverityError today; SeverityWarning is reserved
    Message  string
}
Severity Meaning
SeverityError A problem that prevents the template from being served: the block is dropped at serve time and renders nothing.
SeverityWarning Reserved for problems that degrade a template rather than break it — the home for the data-dependent checks a future sample-data mode would surface. Not emitted today.

Diagnostics are not errors. The returned error is reserved for infrastructure failures — a component set that itself fails to parse, or an internal fault. A template that does not parse is always reported as a Diagnostic, never as an error. This mirrors the shape of a linter: problems in the input are data, not errors.

diags, err := livetemplate.Validate(text)
if err != nil {
    return fmt.Errorf("validation could not run: %w", err) // infrastructure fault
}
for _, d := range diags {
    fmt.Printf("%s:%d: %s: %s\n", path, d.Line, d.Severity, d.Message)
}
if len(diags) > 0 {
    return errors.New("template rejected")
}

What it checks: the syntax and composition layer — unclosed or malformed actions ({{range}}, {{if}}, {{with}}), unknown functions (checked against the framework's own builtins, which a downstream caller cannot enumerate, so this check cannot be reproduced outside the module), and unresolved component or composition templates.

What it does not check: data-dependent problems that only surface when the template is executed against a value. Those are out of scope until a sample-data mode is added.

At most one diagnostic per call today. The underlying parser stops at the first error rather than recovering, so the slice has length 0 or 1. The slice shape anticipates the multi-error reporting a recovering pass could add.

Validating templates that use components

func WithValidateComponents(sets ...*TemplateSet) ValidateOption

Makes the given component template sets available, so a template invoking {{template "ns:name" .}} resolves the same way it does at serve time. Pass the same sets you pass to New(WithComponentTemplates(...)); without them a component reference is reported — correctly — as an unresolved template.

diags, err := livetemplate.Validate(text,
    livetemplate.WithValidateComponents(uiKit),
)

Each call re-parses the supplied component sets from their fs.FS. That suits pre-serve and dev-time validation rather than per-keystroke use against a large component library.

Line-number caveat: HTML comments are stripped before parsing (matching serve), so a diagnostic below a multi-line HTML comment can report a line shifted by the comment's height.


Controller+State Pattern

For patterns, examples, and usage guide, see Controller+State Pattern.

State Interface

type State interface {
    encoding.BinaryMarshaler
    encoding.BinaryUnmarshaler
    Inner() any
}

State represents serializable session data. Use AsState[T]() instead of implementing directly.

AsState

func AsState[T any](s *T) State

Wraps a plain struct pointer to satisfy the State interface using JSON serialization.

Lifecycle Methods

Controllers may implement these optional lifecycle methods:

// Called on every HTTP request (GET and POST) and every WebSocket connect (new and reconnect)
func (c *Controller) Mount(state S, ctx *livetemplate.Context) (S, error)

// Called on each WebSocket connect (including reconnects)
func (c *Controller) OnConnect(state S, ctx *livetemplate.Context) (S, error)

// Called when a WebSocket disconnects
func (c *Controller) OnDisconnect()

Action Methods

Action methods handle user interactions. They receive the current state and return the modified state:

func (c *Controller) ActionName(state S, ctx *livetemplate.Context) (S, error)

Action dispatch is automatic: <button name="addItem"> dispatches to AddItem().

Struct Tags

Tag Description
lvt:"persist" Field is persisted to SessionStore, survives page refresh. Fields without this tag are ephemeral.

Testing

func AssertPureState[T any](t *testing.T)

Validates that a state type contains only serializable data (no *sql.DB, *slog.Logger, etc.).


Context

Context is the unified context for all lifecycle and action methods. It embeds context.Context.

Constructor

func NewContext(ctx context.Context, action string, data map[string]interface{}) *Context

Primarily useful in tests. In production, Context is created internally and passed to controller methods.

Action and Identity

Method Signature Description
Action () string Returns the action name that triggered this context
UserID () string Returns the authenticated user's ID
Session () Session Returns the Session for server-initiated actions
IsHTTP () bool Whether this is an HTTP (not WebSocket) context
SelfTopic () string Returns the reserved-namespace topic (lvt:session:<groupID>) that identifies this session's own connections. ACL-exempt — always subscribable.
Subscribe (topic string) error Subscribes the calling connection to a topic. SelfTopic() bypasses the ACL; developer topics run WithTopicACL. Returns *TopicForbiddenError when the ACL denies.
Unsubscribe (topic string) error Removes the calling connection's subscription.
Publish (topic, action string, data map[string]interface{}) Queues a named action dispatch to every connection subscribed to the topic. Fan-out is opt-in — only Subscribers receive it. Deferred until the current action completes successfully.

Data Extraction

Method Signature Description
GetString (key string) string Get a string value from action data
GetInt (key string) int Get an integer value
GetFloat (key string) float64 Get a float value
GetBool (key string) bool Get a boolean value
Has (key string) bool Check if a key exists in action data
Get (key string) interface{} Get a raw value
Bind (v interface{}) error Unmarshal action data into a struct
BindAndValidate (v interface{}, validate *validator.Validate) error Bind and validate in one step

HTTP Operations

These methods return ErrNoHTTPContext when called from a WebSocket action.

Method Signature Description
SetCookie (cookie *http.Cookie) error Set an HTTP cookie
DeleteCookie (name string) error Delete an HTTP cookie
GetCookie (name string) (*http.Cookie, error) Get an HTTP cookie
Redirect (url string, code int) error Send an HTTP redirect (3xx only, relative paths only)

Upload Access

Method Signature Description
HasUploads (name string) bool Check if uploads exist for a field
GetCompletedUploads (name string) []*UploadEntry Get completed upload entries

Flash Messages

func (c *Context) SetFlash(key, message string, opts ...FlashOption)
func (c *Context) ClearFlash(key string)
func FlashExpiry(d time.Duration) FlashOption

Manages flash messages available in templates via .lvt.Flash(key). Common keys: "success", "error", "info", "warning".

Flash persists until explicitly cleared with ClearFlash (or until FlashExpiry elapses). Background updates such as TriggerAction or scan-loop refreshes do not touch flash. See Flash Message Lifecycle for the full lifecycle, multi-tab behavior, and v0.8 → v0.9 migration guidance.

Context Builders

Method Signature Description
WithUserID (userID string) *Context Returns new Context with user ID
WithSession (session Session) *Context Returns new Context with session
WithHTTP (w http.ResponseWriter, r *http.Request) *Context Returns new Context with HTTP
WithAction (action string) *Context Returns new Context with action name
WithData (data map[string]interface{}) *Context Returns new Context with data
WithUploads (uploads UploadAccessor) *Context Returns new Context with uploads
WithFlashSetter (setter FlashSetter) *Context Returns new Context with flash setter

Session

type Session interface {
    TriggerAction(action string, data map[string]interface{}) error
}

Enables server-initiated actions for every connection in the current session group. Use cases: timers, background job notifications, webhook-triggered updates.

ScopeSession.TriggerAction targets a session group (groupID), not a user identity (userID). For the typical anonymous flow where each browser session maps to one group via cookie, this is equivalent to "all tabs of this browser". For authenticated flows the mapping depends on how the Authenticator assigns groupIDs:

Accessed via ctx.Session() inside your controller's OnConnect(state, ctx) lifecycle method (or any action method). The returned Session handle can be captured and used from background goroutines. See Server Actions for examples.


Async

func Async[S any, R any](
    ctx *Context,
    work  func(context.Context) (R, error),
    apply func(s S, result R, err error) (S, error),
)

Runs work off the connection event loop, then re-enters the loop to apply its result to the current session state and re-render the originating connection. Reduces the manual two-action loading pattern from ~15 lines / 2 methods to ~7 lines / 1 method.

Example:

func (c *Controller) Greet(state State, ctx *livetemplate.Context) (State, error) {
    state.Loading = true
    name := strings.TrimSpace(ctx.GetString("name"))
    livetemplate.Async(ctx,
        func(ctx context.Context) (string, error) {
            time.Sleep(700 * time.Millisecond) // simulate slow work
            return name, nil
        },
        func(s State, name string, err error) (State, error) {
            s.Name = name
            s.Loading = false
            return s, nil
        },
    )
    return state, nil // render #1: Loading=true (spinner on)
    // render #2 happens automatically when work completes: Loading=false
}

Scope: Async is supported only inside action handlers (e.g. Greet, Save) that run on the per-connection WebSocket event loop. Calling Async in Mount(), OnConnect(), dispatched actions, server- initiated actions, upload handlers, or HTTP POST handlers logs a warning and drops the operation — there is no persistent connection or event loop to re-enter.

{{.lvt.Pending}}: A framework-provided template variable that is true on the render that registered async work and false on all other renders. It has per-render semantics: if another action or a peer dispatch triggers a render on the same connection while async work is still in flight, that render will see Pending=false (it did not register async work). For loading indicators that must stay visible across interleaved renders, use an explicit Loading bool field in your state instead. Use {{.lvt.Pending}} when the loading indicator is purely visual and single-action flows are the norm:

<button name="greet" {{if .lvt.Pending}}disabled{{end}}>
    {{if .lvt.Pending}}Loading...{{else}}Greet{{end}}
</button>

See Loading States §7.3 for the full comparison of loading approaches.


LiveHandler

type LiveHandler interface {
    http.Handler
    Shutdown(ctx context.Context) error
    MetricsHandler() http.Handler
    Publish(topic, action string, data map[string]interface{}) error
    Func() http.HandlerFunc
}

Returned by Template.Handle(). Serves both HTTP and WebSocket requests.

Method Description
ServeHTTP Handles HTTP requests and WebSocket upgrades
Shutdown Gracefully drains connections with context timeout
MetricsHandler Returns Prometheus metrics endpoint handler
Publish Fans a topic action out to subscribers from outside an action handler (PubSub)
Func Returns ServeHTTP as an http.HandlerFunc

Standard library integration

A LiveHandler is an ordinary net/http handler — one ServeHTTP serves the initial GET render, form-action POSTs, and the WebSocket upgrade. It composes with net/http routing directly, and Func() covers the entry points that take a function instead of an http.Handler:

handler := tmpl.Handle(&CounterController{}, livetemplate.AsState(&CounterState{}))

http.Handle("/counter", handler)                  // http.Handler
mux.Handle("/counter", handler)                   // ServeMux
http.HandleFunc("/counter", handler.Func())       // http.HandlerFunc
mux.HandleFunc("GET /counter", handler.Func())    // Go 1.22+ method patterns
mux.HandleFunc("POST /counter", handler.Func())   // form actions still dispatch

Func() is exactly handler.ServeHTTP — neither form is preferred, and taking it does not give up Shutdown, Publish or MetricsHandler, which stay on the LiveHandler it came from.

Mounting under a subtree with http.StripPrefix is supported; the handler reads the request path as rewritten.

Middleware must forward http.Hijacker. A WebSocket upgrade takes over the raw connection, so it needs the http.ResponseWriter to implement http.Hijacker. Middleware that only observes the request is fine; middleware that wraps the writer (logging, gzip, status capture) hides Hijack unless it forwards the method:

// Breaks the upgrade: embedding promotes Write/Header/WriteHeader, not Hijack.
type wrapped struct{ http.ResponseWriter }

The failure is partial and easy to miss — GET and POST keep rendering, only the upgrade is refused with a 500 — so a failed upgrade logs a hint naming middleware whenever the writer is not hijackable. To keep such middleware, either forward Hijack to the underlying writer, or leave the writer unwrapped when livetemplate.WSIsUpgrade(r) is true.

The hint is attached on the writer's own defect, which need not be what the accompanying error reports — an upgrader can reject a handshake earlier (a disallowed Origin, say) and never reach the hijack. Read it as a second failure the upgrade would have hit regardless, not as the reported cause.


Authentication

Authenticator Interface

type Authenticator interface {
    Identify(r *http.Request) (userID string, err error)
    GetSessionGroup(r *http.Request, userID string) (groupID string, err error)
}

Maps HTTP requests to user IDs and session groups.

Built-in Authenticators

AnonymousAuthenticator (default): Browser-based session grouping via persistent cookie. All tabs in the same browser share state. No configuration needed.

BasicAuthenticator: HTTP Basic Auth with user-provided validation.

Security: Always use HTTPS. No built-in rate limiting or account lockout -- enforce brute-force protection externally.

func NewBasicAuthenticator(validateFunc func(username, password string) (bool, error)) *BasicAuthenticator

Session Stores

SessionStore Interface

type SessionStore interface {
    Get(ctx context.Context, groupID string) interface{}
    Set(ctx context.Context, groupID string, state interface{})
    Delete(ctx context.Context, groupID string)
    List(ctx context.Context) []string
}

SingleStoreSetter Interface

Optional optimization for targeted persistence of individual stores:

type SingleStoreSetter interface {
    SetStore(ctx context.Context, groupID string, storeName string, store interface{})
}

MemorySessionStore

In-memory store with automatic cleanup. Suitable for single-instance deployments.

func NewMemorySessionStore(opts ...SessionStoreOption) *MemorySessionStore
Option Default Description
WithCleanupTTL(ttl) 24h TTL for inactive session groups
WithCleanupInterval(interval) 1h Cleanup goroutine interval

RedisSessionStore

Redis-backed store for distributed deployments. Supports Redis, Redis Cluster, Ring, and Sentinel.

func NewRedisSessionStore(client redis.UniversalClient, opts ...RedisSessionStoreOption) *RedisSessionStore
Option Default Description
WithSessionTTL(ttl) 24h Session expiry in Redis
WithMaxRetries(n) 3 Retry attempts with exponential backoff
WithRetryDelay(delay) 100ms Base delay between retries

Both implementations satisfy SingleStoreSetter.


Configuration

Environment Variables

func LoadEnvConfig() (*EnvConfig, error)

Load configuration from environment variables with LVT_ prefix. Call config.ToOptions()... to convert to template options.

Variable Type Default Description
LVT_MAX_CONNECTIONS int64 0 (unlimited) Max concurrent WebSocket connections
LVT_MAX_CONNECTIONS_PER_GROUP int64 0 (unlimited) Max connections per session group
LVT_ALLOWED_ORIGINS string "" Comma-separated allowed WebSocket origins
LVT_DEV_MODE bool false Enable development mode
LVT_WEBSOCKET_DISABLED bool false HTTP-only mode
LVT_LOADING_DISABLED bool false Disable automatic loading indicator
LVT_TEMPLATE_BASE_DIR string "" Base directory for template discovery
LVT_PROGRESSIVE_ENHANCEMENT bool true Enable non-JS form submission
LVT_WS_BUFFER_SIZE int 50 WebSocket send buffer size per connection

Template Option Functions

Options passed to New():

Option Signature Description
WithDevMode (enabled bool) Enable dev mode: allows all WS origins, verbose logging, {{.lvt.DevMode}}
WithSessionStore (store SessionStore) Set session store
WithAuthenticator (auth Authenticator) Set authenticator
WithAllowedOrigins (origins []string) Allowed WebSocket origins
WithPermissiveOriginCheck () Bypass origin check without dev mode (WithDevMode already relaxes origins)
WithMaxConnections (max int64) Max WebSocket connections
WithMaxConnectionsPerGroup (max int64) Max connections per group
WithWebSocketDisabled () HTTP-only mode
WithWebSocketBufferSize (size int) WebSocket send buffer size
WithLoadingDisabled () Disable loading indicator
WithMessageRateLimit (messagesPerSecond float64, burstCapacity int) WebSocket rate limiting
WithCookieMaxAge (maxAge time.Duration) Session cookie max age
WithUpgrader (upgrader *websocket.Upgrader) Custom WebSocket upgrader
WithParseFiles (files ...string) Explicit template files
WithParseFS (fsys fs.FS, patterns ...string) Templates from an fs.FS (e.g. embed.FS); precedence over WithParseFiles
WithTemplateBaseDir (dir string) Template discovery base dir
WithIgnoreTemplateDirs (dirs ...string) Skip directories during discovery
WithUpload (name string, config UploadConfig) Configure upload field
WithPubSubBroadcaster (broadcaster pubsub.Broadcaster) Enable cross-instance peer fan-out via Redis Pub/Sub
WithComponentTemplates (sets ...*TemplateSet) Register component templates
WithProgressiveEnhancement (enabled bool) Non-JS form submission support

Uploads

UploadConfig

type UploadConfig struct {
    Accept      []string  // Allowed MIME types or extensions
    MaxEntries  int       // Max concurrent files (0 = unlimited)
    MaxFileSize int64     // Max file size in bytes (0 = unlimited)
    AutoUpload  bool      // Start upload on file selection
    ChunkSize   int       // WebSocket chunk size (default: 256KB)
    External    Presigner // Optional presigner for direct-to-storage uploads
}

UploadEntry

type UploadEntry struct {
    ID          string
    ClientName  string    // Original filename
    ClientType  string    // MIME type
    ClientSize  int64     // File size in bytes
    Progress    int       // 0-100
    Done        bool      // Upload completed
    Error       string    // Error message
    TempPath    string    // Server-side temp file (server uploads)
    ExternalRef string    // Storage reference (external uploads)
}

Presigner

type Presigner interface {
    Presign(entry *UploadEntry) (UploadMeta, error)
}

type UploadMeta struct {
    Uploader string            // Provider name (e.g., "s3")
    URL      string            // Presigned upload URL
    Fields   map[string]string // Form fields for multipart POST
    Headers  map[string]string // HTTP headers for PUT
}

For complete upload documentation including S3 configuration, see Upload Reference.


Health Checks

HealthHandler

func NewHealthHandler(timeout time.Duration) *HealthHandler

Provides Kubernetes-ready /health/live and /health/ready endpoints. Register checkers via health.RegisterChecker(name, checker).

HealthChecker Interface

type HealthChecker interface {
    Check(ctx context.Context) error
}

Built-in: NewSessionStoreHealthChecker(store), NewRedisHealthChecker(store).


PubSub (Cross-Instance Peer Fan-Out)

Package pubsub provides cross-instance messaging for horizontally scaled deployments. See the PubSub Reference for the complete API including Broadcaster, DynamicSubscriber, the livetemplate:broadcast:* channel namespace, channel schema, and subscription lifecycle.


Error Types

Sentinel Errors

Error Description
ErrNoHTTPContext HTTP method called from WebSocket action
ErrInvalidRedirectCode Redirect status code is not 3xx
ErrInvalidRedirectURL Redirect URL is not a valid relative path
ErrMethodNotFound No controller method matches the action name

FieldError

type FieldError struct {
    Field   string
    Message string
}

func NewFieldError(field string, err error) FieldError

MultiError

type MultiError []FieldError

A collection of field errors. Implements the error interface. Return from action methods to display per-field validation errors in templates.

ValidationToMultiError

func ValidationToMultiError(err error) MultiError

Converts go-playground/validator errors to MultiError.


See Also

source: livetemplate/livetemplate · path: docs/references/api-reference.md · ref: v0.23.0 · commit: 8294ce439a46