livetemplate

package module
v0.8.18 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 14, 2026 License: MIT Imports: 51 Imported by: 4

README

LiveTemplate

Build interactive web applications in Go using a simplified programming model. Write server-side code, get reactive UIs automatically.

Quick StartAPI DocsCLI ToolExamples


ALPHA SOFTWARE

LiveTemplate is currently in alpha stage. Core features work and are tested, but the API may change before v1.0. Use in production at your own risk.


A Better Way to Build Interactive Apps

Every interactive feature in a traditional web app requires the same ceremony: design a REST endpoint, write a serializer, manage client-side state, update the DOM, and wire it all together. That overhead discourages interactivity — teams leave things static not because they should be, but because the plumbing isn't worth it. As Chris McCord put it when explaining why he built Phoenix LiveView: conventional frameworks make you "fetch the world, munge it into some format, and shoot it over the wire... then throw all that state away" on every request.

LiveView's answer was to keep all state on the server and push rendered updates over a persistent connection — no REST layer, no client-side framework. LiveTemplate brings that approach to Go, but with one major difference: it doesn't require a persistent connection and works equally well over standard HTTP. When a user clicks a button, LiveTemplate calls a method on your Go struct, you update your state, and only what changed is sent back to the browser. No endpoints to design, no JSON to serialize, no client-side state to synchronize.

sequenceDiagram
    participant Browser
    participant Server

    Browser->>Server: User clicks button<br/>{action: "increment"}
    Note over Server: s.Counter++<br/>(Counter: 5 → 6)
    Note over Server: Tree diff calculated<br/>Only Counter changed → {"0": "6"}
    Server->>Browser: {"0": "6"}
    Note over Browser: DOM updated<br/>Counter: 6

Why LiveTemplate?

1. Reactive UIs in Pure Go

All user-initiated interactions work over plain HTTP — no WebSocket required. The JS client sends actions as standard HTTP requests and patches the DOM with the response. WebSocket is only needed when the server needs to push updates unprompted (e.g., notifying other users in a chat room). Unlike Phoenix LiveView, which requires a persistent connection for all interactions, LiveTemplate treats WebSocket as an optional upgrade for broadcast scenarios only.

This extends to progressive enhancement at the HTML level too. LiveTemplate follows a progressive complexity model — start with standard HTML, add custom attributes only when you need behaviors HTML can't express:

Tier What you write When to use
Tier 1: Standard HTML <form>, <button name="add">, <dialog>, <a href> Forms, actions, modals, navigation
Tier 2: lvt-* attributes lvt-debounce, lvt-key, lvt-addClass-on:pending Timing, keyboard shortcuts, reactive DOM

A form that works at all transport levels — no JS, fetch, and WebSocket — with zero custom attributes:

<form method="POST">
    <input type="text" name="title" required placeholder="What needs to be done?">
    <button name="add">Add Todo</button>
</form>

The button's name IS the action — <button name="add"> routes to the Add() method. Without JS, the form POSTs normally. With the JS client, the submission is intercepted and the DOM is patched in place. One declaration, three transport modes.

func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
    if err := ctx.ValidateForm(); err != nil {  // Inferred from HTML required attr
        return state, err
    }
    state.Items = append(state.Items, Todo{Title: ctx.GetString("title")})
    return state, nil
}

Forms without a named button auto-route to a conventional Submit() method — no attributes needed at all for the simplest case. See the Progressive Complexity Guide for the full walkthrough and the todos-progressive and profile-progressive examples.

For behaviors that HTML can't express — timing control, reactive DOM, keyboard shortcuts — use lvt-* attributes:

<!-- Debounced search: waits 300ms after typing stops before querying -->
<input name="Query" value="{{.Query}}"
    lvt-input="search" lvt-debounce="300"
    placeholder="Search...">
func (c *AppController) Search(state AppState, ctx *livetemplate.Context) (AppState, error) {
    state.Query = ctx.GetString("Query")
    state.Results = c.DB.Search(state.Query)
    return state, nil
}

Your existing Go toolchain, testing infrastructure, and deployment pipeline all work as-is. See the examples repository.

2. Safe State Management

LiveTemplate separates controllers (singleton, holds dependencies) from state (pure data, cloned per session). This prevents a class of bugs where session-specific data like OAuth tokens or caches accidentally leaks between users:

// Controller: Singleton, holds shared dependencies — never cloned
type TodoController struct {
    DB     *sql.DB
    Logger *slog.Logger
}

// State: Pure data, cloned per session — must be serializable
type TodoState struct {
    Items  []Todo
    Filter string
}

func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
    todo := Todo{Title: ctx.GetString("title")}
    // c.DB is a dependency on the controller, not in cloned state
    c.DB.Create(&todo)
    state.Items = append(state.Items, todo)
    return state, nil
}

The separation is enforced at the API level: tmpl.Handle(controller, livetemplate.AsState(state)). Convention and the AssertPureState[T]() test helper catch accidental dependency leakage. See the chat example for a multi-user app using this pattern with broadcasting.

3. Efficient by Design

LiveTemplate separates your template into static HTML (the parts that never change) and dynamic values (the parts that do). On first render, the client receives and caches the full structure:

{"s": ["<div>Counter: ", "</div>"], "0": "5"}

When the counter changes from 5 to 6, only the new value is sent:

{"0": "6"}

No re-rendered HTML, no string diffing — just the single value that changed. For typical pages with lots of markup and few changing values, this means 50-90% less data than sending full HTML. This optimization works over both plain HTTP and WebSocket — the server tracks tree state per session, so subsequent actions always send minimal diffs regardless of transport. This is the same static/dynamic split that Phoenix LiveView uses — a proven approach to minimizing wire traffic.

4. Idiomatic Go Error Handling

Errors flow naturally using Go's familiar patterns. Return an error and LiveTemplate automatically displays it in your template:

func (c *AuthController) Signup(state AuthState, ctx *livetemplate.Context) (AuthState, error) {
    var input SignupInput
    if err := ctx.BindAndValidate(&input, validate); err != nil {
        return state, err  // Validation errors automatically sent to client
    }

    if c.usernameExists(input.Username) {
        return state, livetemplate.NewFieldError("username",
            errors.New("username already taken"))
    }

    return state, nil
}
<input name="username" {{if .lvt.HasError "username"}}aria-invalid="true"{{end}}>
{{if .lvt.HasError "username"}}
    <small>{{.lvt.Error "username"}}</small>
{{end}}

No error serialization code. No client-side error state management. Actions return (State, error) — the standard Go signature.

5. Generate Complete Apps Instantly

The lvt CLI generates complete CRUD applications — forms, tables, validation, database integration — all reactive by default:

lvt new myapp
cd myapp
lvt gen products name price:float stock:int
lvt serve

Code generation works reliably because templates have a predictable static/dynamic structure. Generated code inherits the reactive programming model. No glue code, no manual wiring. Pluggable CSS kits (Tailwind, Bulma, Pico, or plain HTML) let you match your team's preferred styling approach.

Current Limitations

LiveTemplate is inspired by Phoenix LiveView but doesn't yet cover its full feature set. Here's what's missing:

Feature LiveView LiveTemplate Notes
Live Navigation push_navigate, push_patch Not yet LiveView updates the URL and swaps page sections without a full reload. LiveTemplate requires full page navigation.
Stateful Components LiveComponent with own lifecycle Stateless templates only LiveView components have isolated state, events, and lifecycle hooks. LiveTemplate has {{template}} invocations but no component-level state or event handling.
Streams stream/3 for large lists Not yet LiveView streams handle large/infinite lists without keeping all items in server memory. LiveTemplate keeps all state in memory.
JS Commands JS.push, JS.toggle, JS.show Partial LiveView has composable server-defined JS command chains. LiveTemplate's reactive attributes cover common cases (disable, add/remove class, set attribute) but aren't as flexible.
Client Hooks phx-hook lifecycle callbacks Not yet LiveView hooks let you integrate third-party JS libraries (charts, maps, editors) with mounted/updated/destroyed callbacks.
Presence Phoenix.Presence Not built-in LiveView has built-in distributed presence tracking ("who's online"). Can be built on LiveTemplate's session stores but requires manual implementation.
Testing Helpers live/2, render_click/3 Minimal LiveView provides a full test DSL for simulating user interactions without a browser. LiveTemplate has AssertPureState but no view-level test helpers yet.
Form Recovery Automatic on reconnect Not yet LiveView restores form inputs automatically after a WebSocket reconnection.

Some of these are on the roadmap. If a missing feature is blocking your project, open an issue — it helps us prioritize.

Quick Start

go get github.com/livetemplate/livetemplate

1. Create your controller and state (main.go)

// State: Pure data, cloned per session
type CounterState struct {
    Counter int
}

// Controller: Holds dependencies, singleton
type CounterController struct{}

// Action "increment" maps to method Increment()
func (c *CounterController) Increment(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Counter++
    return state, nil
}

// Action "decrement" maps to method Decrement()
func (c *CounterController) Decrement(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Counter--
    return state, nil
}

func main() {
    controller := &CounterController{}
    state := &CounterState{Counter: 0}
    tmpl := livetemplate.New("counter")
    http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(state)))
    http.ListenAndServe(":8080", nil)
}

2. Create your template (counter.tmpl)

<!-- counter.tmpl -->
<h1>Counter: {{.Counter}}</h1>
<form method="POST" style="display:inline">
    <button name="increment">+</button>
    <button name="decrement">-</button>
</form>

<script src="https://cdn.jsdelivr.net/npm/@livetemplate/client@latest/dist/livetemplate-client.browser.js"></script>

3. Run it

go run main.go  # Open http://localhost:8080

That's it! Click buttons and watch the counter update automatically.

How It Works

User clicks button → Server updates state → Template re-renders →
Only changed values sent → Client patches the DOM
  1. Define your State as a Go struct (pure data, cloned per session)
  2. Define your Controller with dependencies (singleton)
  3. Handle actions as methods on the Controller (action name → method name)
  4. Use standard Go templates (add lvt-* attributes only when needed)
  5. LiveTemplate automatically syncs state to UI

State is persisted to SessionStore after every action. Cross-tab sync is automatic when the controller implements a Sync() method — the framework dispatches it to peer connections after each action. Use ctx.BroadcastAction() for custom cross-connection updates.

All interactive features — including efficient tree-based diffs — work over plain HTTP. WebSocket is optional, required only for server-initiated broadcasts (e.g., multi-user chat notifications).

Performance

LiveTemplate is designed for high-performance reactive updates with minimal bandwidth usage.

Key Metrics
Operation Latency Bandwidth Savings
Initial Render ~20-65µs -
Small Update (1-2 fields) ~18-20µs 85% vs full render
Large Update (5+ fields) ~65µs 65% vs full render
Range Operations ~30-65µs 80% vs full render

Actual benchmarks from baseline (Go 1.26, Apple M1). See baseline.txt for complete results.

How It Works
  1. First Render: Full HTML sent; client caches the static parts
  2. Subsequent Updates: Only changed values sent (static HTML already cached)
  3. Result: 85%+ bandwidth savings, sub-millisecond latency
Running Benchmarks
# Run all benchmarks
make bench

# Compare against baseline
make bench-compare

# Generate performance profiles
make profile-cpu
make profile-mem

See the full performance documentation for comprehensive analysis.

Learn More

Core Documentation:

Feature Guides:

Architecture:

Related Projects:

Contributing

New to the codebase? Start with the Contributor Walkthrough - a comprehensive guide to the 5-phase architecture with links to code and tests.

See CONTRIBUTING.md for development setup and guidelines.

License

MIT License - see LICENSE file for details.


Built with LiveTemplate? Share your project in GitHub Discussions.

Documentation

Overview

Package livetemplate provides a library for building real-time, reactive web applications in Go with minimal code. It uses tree-based DOM diffing to send only what changed over WebSocket or HTTP, inspired by Phoenix LiveView.

Quick Start

Define your application state as a Go struct with methods for each action:

type Counter struct {
    Count int
}

func (c *Counter) Increment(ctx *livetemplate.ActionContext) error {
    c.Count++
    return nil
}

func (c *Counter) Decrement(ctx *livetemplate.ActionContext) error {
    c.Count--
    return nil
}

Actions are automatically dispatched to methods matching the action name (e.g., "increment" → Increment, "add_item" → AddItem).

Create a template with `lvt-on:{event}` attributes for event binding:

<!-- counter.tmpl -->
<h1>Counter: {{.Count}}</h1>
<button lvt-on:click="increment">+</button>
<button lvt-on:click="decrement">-</button>

Wire it up in your main function:

func main() {
    counter := &Counter{Count: 0}
    tmpl := livetemplate.New("counter")
    http.Handle("/", tmpl.Handle(counter))
    http.ListenAndServe(":8080", nil)
}

How It Works

LiveTemplate separates static and dynamic content in templates:

  • Static content (HTML structure, unchanging text) is sent once and cached client-side
  • Dynamic content (data values) is sent on every update as a minimal tree diff
  • This achieves 50-90% bandwidth reduction compared to sending full HTML

The client library (TypeScript) handles WebSocket communication, event delegation, and applying DOM updates efficiently.

Tree-Based Updates

Templates are parsed into a tree structure that separates statics and dynamics:

{
    "s": ["<div>Count: ", "</div>"],  // Statics (cached)
    "0": "42"                          // Dynamic value
}

Subsequent updates only send changed dynamic values:

{
    "0": "43"  // Only the changed value
}

Key Types

  • Template: Manages template parsing, execution, and update generation
  • Store: Interface for application state and action handlers
  • ActionContext: Provides action data and utilities in Change() method
  • ActionData: Type-safe data extraction and validation
  • Broadcaster: Share state updates across all connected clients
  • SessionStore: Per-session state management

Advanced Features

  • Multi-store pattern: Namespace multiple stores in one template
  • Broadcasting: Real-time updates to all connected clients
  • Server-side validation: Automatic error handling with go-playground/validator
  • Form lifecycle events: Client-side hooks for pending, success, error, done
  • Focus preservation: Maintains input focus and scroll position during updates

For complete documentation, see https://github.com/livetemplate/livetemplate

Index

Examples

Constants

View Source
const (
	WSTextMessage   = 1
	WSBinaryMessage = 2
	WSCloseMessage  = 8
	WSPingMessage   = 9
	WSPongMessage   = 10
)

WebSocket message types (RFC 6455, Section 11.8)

View Source
const (
	WSCloseNormalClosure   = 1000
	WSCloseGoingAway       = 1001
	WSCloseProtocolError   = 1002
	WSCloseAbnormalClosure = 1006
	WSCloseServiceRestart  = 1012
)

WebSocket close codes (RFC 6455, Section 7.4)

View Source
const CapabilityChange = "change"

CapabilityChange is the capability name for controllers with a Change() method.

View Source
const MaxBroadcastsPerAction = 100

BroadcastAction queues a broadcast to all other connections in the same session group. The named action is dispatched on each receiving connection after the current action completes successfully.

Each receiving connection runs the named action with its own per-connection state via DispatchWithState, preserving per-connection fields (e.g., CurrentUser).

Broadcasts are deferred: they execute only after the triggering action returns without error. If the action returns an error, queued broadcasts are discarded.

Constraints:

  • Dispatched actions run with context.Background() — middleware-injected request values (auth tokens, tracing spans) are not available.
  • BroadcastAction calls inside a dispatched action are ignored to prevent infinite broadcast storms.
  • Context.With*() methods create shallow copies. Broadcasts queued after the copy diverge (append allocates a new backing array once capacity is exceeded).

Example:

func (c *ChatController) Send(state ChatState, ctx *livetemplate.Context) (ChatState, error) {
    c.mu.Lock()
    c.messages = append(c.messages, msg)
    c.mu.Unlock()
    state.Messages = c.copyMessages()
    ctx.BroadcastAction("RefreshMessages", nil)
    return state, nil
}

MaxBroadcastsPerAction is the maximum number of BroadcastAction calls allowed per action invocation. Excess calls are dropped with an error log.

Variables

View Source
var (
	// ErrNoHTTPContext is returned when HTTP methods (SetCookie, Redirect, etc.)
	// are called from a WebSocket action. These methods require an HTTP response
	// writer which is not available in WebSocket contexts.
	//
	// To set cookies or redirect, use HTTP POST forms instead of WebSocket actions.
	// This is consistent with security best practices - session cookies should be
	// HttpOnly and can only be set via HTTP responses, not JavaScript/WebSocket.
	ErrNoHTTPContext = errors.New("HTTP methods require HTTP context (not available in WebSocket actions)")

	// ErrInvalidRedirectCode is returned when Redirect is called with a non-3xx status code.
	ErrInvalidRedirectCode = errors.New("invalid redirect status code (must be 3xx)")

	// ErrInvalidRedirectURL is returned when Redirect is called with a potentially
	// unsafe URL that could lead to open redirect vulnerabilities.
	ErrInvalidRedirectURL = errors.New("invalid redirect URL (must be relative path starting with /)")
)

HTTP context errors for ActionContext methods

View Source
var ErrMethodNotFound = errors.New("method not found for action")

ErrMethodNotFound is returned when Dispatch cannot find a method matching the action.

Functions

func AssertPureState added in v0.7.0

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

AssertPureState validates that a state type contains only serializable data. Use in tests to catch accidental dependency inclusion:

func TestMyState_IsPure(t *testing.T) {
    AssertPureState[MyState](t)
}

func DispatchWithState added in v0.7.0

func DispatchWithState(controller interface{}, state interface{}, ctx *Context) (interface{}, error)

DispatchWithState routes an action to a controller method with new signature.

Method signature: func(state StateType, ctx *Context) (StateType, error)

Returns the modified state and any error from the method. The controller is a singleton that holds dependencies. State is passed by value and a new state is returned.

Example:

type CounterController struct { DB *sql.DB }
func (c *CounterController) Increment(state CounterState, ctx *Context) (CounterState, error) {
    state.Count++
    return state, nil
}

func HasActionMethod added in v0.8.6

func HasActionMethod(controller interface{}, state interface{}, action string) bool

HasActionMethod checks if a controller has a method that can handle the given action. Uses the same signature validation as DispatchWithState: func(state, *Context) (state, error). Automatically dereferences pointer state types to match the value type used by dispatch.

func WSCloseStatusText added in v0.8.5

func WSCloseStatusText(code int) string

WSCloseStatusText returns a text description for the close code.

func WSFormatCloseMessage added in v0.8.5

func WSFormatCloseMessage(closeCode int, text string) []byte

WSFormatCloseMessage builds a WebSocket close frame payload. Per RFC 6455 §5.5, the close reason must be <= 123 bytes (125 - 2 for code).

func WSIsUnexpectedCloseError added in v0.8.5

func WSIsUnexpectedCloseError(err error, expectedCodes ...int) bool

WSIsUnexpectedCloseError reports whether err is a WebSocket close error with a code not in the list of expected codes.

func WSIsUpgrade added in v0.8.5

func WSIsUpgrade(r *http.Request) bool

WSIsUpgrade reports whether the HTTP request is a WebSocket upgrade request per RFC 6455 §4.1 (requires GET method).

Types

type ActionData

type ActionData struct {
	// contains filtered or unexported fields
}

ActionData wraps action data with utilities for binding and validation

func NewActionData added in v0.5.0

func NewActionData(data map[string]interface{}) *ActionData

NewActionData creates ActionData from a map This is the public version for use by external packages like livepage

func (*ActionData) Bind

func (a *ActionData) Bind(v interface{}) error

Bind unmarshals the data into a struct

func (*ActionData) BindAndValidate

func (a *ActionData) BindAndValidate(v interface{}, validate *validator.Validate) error

BindAndValidate binds data to struct and validates it in one step

func (*ActionData) Get

func (a *ActionData) Get(key string) interface{}

Get returns the raw value for a key

func (*ActionData) GetBool

func (a *ActionData) GetBool(key string) bool

GetBool extracts a bool value. Returns false if key doesn't exist or value is not a bool.

DEPRECATED: Use GetBoolOk for explicit error handling to distinguish between missing keys, type errors, and actual false values. This method will be removed in v0.3.0.

func (*ActionData) GetBoolOk added in v0.1.3

func (a *ActionData) GetBoolOk(key string) (bool, bool)

GetBoolOk extracts a bool value with explicit success indicator. Returns (value, true) if key exists and value is a bool or boolean string. Returns (false, false) if key doesn't exist or value cannot be parsed as bool.

This method handles both boolean values and string values "true"/"false" from HTML form submissions (HTTP path uses strings, WebSocket uses booleans). String comparison is case-insensitive to handle variations like "True", "TRUE", etc.

func (*ActionData) GetFloat

func (a *ActionData) GetFloat(key string) float64

GetFloat extracts a float64 value. Returns 0 if key doesn't exist or value is not a number.

DEPRECATED: Use GetFloatOk for explicit error handling to distinguish between missing keys, type errors, and actual zero values. This method will be removed in v0.3.0.

func (*ActionData) GetFloatOk added in v0.1.3

func (a *ActionData) GetFloatOk(key string) (float64, bool)

GetFloatOk extracts a float64 value with explicit success indicator. Returns (value, true) if key exists and value is a number or numeric string. Returns (0, false) if key doesn't exist or value cannot be parsed as float.

Accepts native Go numeric types (int, int32, int64, float32, float64, etc.) and numeric strings from form fields and data-* attributes. Native numeric support matters for Session.TriggerAction, where callers pass Go-native values rather than JSON-unmarshaled ones.

Precision note: float64 has a 53-bit mantissa, so integer values larger than 2^53 (int64 and uint64) lose precision during conversion. Callers needing exact large integer round-trips should use GetInt instead.

func (*ActionData) GetInt

func (a *ActionData) GetInt(key string) int

GetInt extracts an int value (JSON numbers are float64). Returns 0 if key doesn't exist or value is not a number.

DEPRECATED: Use GetIntOk for explicit error handling to distinguish between missing keys, type errors, and actual zero values. This method will be removed in v0.3.0.

func (*ActionData) GetIntOk added in v0.1.3

func (a *ActionData) GetIntOk(key string) (int, bool)

GetIntOk extracts an int value with explicit success indicator. Returns (value, true) if key exists and value is a number or numeric string. Returns (0, false) if key doesn't exist or value cannot be parsed as int.

Accepts native Go numeric types (int, int32, int64, float32, float64, etc.) and numeric strings from form fields and data-* attributes. Native numeric support matters for Session.TriggerAction, where callers pass Go-native values rather than JSON-unmarshaled ones.

Overflow and bounds safety:

  • Unsigned integers that exceed math.MaxInt on the current platform return (0, false) rather than silently wrapping to a negative int. This matters on 64-bit platforms for values in (math.MaxInt64, math.MaxUint64] and on 32-bit platforms for values in (math.MaxInt32, math.MaxUint32].
  • int64 values outside [math.MinInt, math.MaxInt] return (0, false); this only matters on 32-bit platforms.
  • float values that are NaN, infinite, out of the int range, or non-integer (e.g. 1.7) return (0, false). Silently truncating a float to an int would hide caller mistakes when a map entry meant for a string field or a float-typed field gets routed to GetInt.

func (*ActionData) GetString

func (a *ActionData) GetString(key string) string

GetString extracts a string value. Returns empty string if key doesn't exist or value is not a string.

DEPRECATED: Use GetStringOk for explicit error handling to distinguish between missing keys, type errors, and actual empty strings. This method will be removed in v0.3.0.

func (*ActionData) GetStringOk added in v0.1.3

func (a *ActionData) GetStringOk(key string) (string, bool)

GetStringOk extracts a string value with explicit success indicator. Returns (value, true) if key exists and value is a string or number. Returns ("", false) if key doesn't exist or value cannot be converted to string.

This method handles both string values and JSON numbers (float64), since the client-side parseValue() may convert numeric strings like "1" to numbers.

func (*ActionData) Has

func (a *ActionData) Has(key string) bool

Has checks if a key exists

func (*ActionData) Raw

func (a *ActionData) Raw() map[string]interface{}

Raw returns the underlying map for direct access

type AnonymousAuthenticator

type AnonymousAuthenticator struct{}

AnonymousAuthenticator provides browser-based session grouping for anonymous users.

This is the default authenticator and implements the most common use case: - All tabs in the same browser share data (same groupID) - Different browsers have independent data (different groupID) - No user authentication required (userID is always "")

The groupID is stored in a persistent cookie ("livetemplate-id") that survives browser restarts and lasts for 1 year. This provides seamless multi-tab experience without requiring user login.

Example behavior:

  • User opens Tab 1 in Chrome → groupID = "anon-abc123"
  • User opens Tab 2 in Chrome → groupID = "anon-abc123" (same cookie, shares state)
  • User opens Tab 3 in Firefox → groupID = "anon-xyz789" (different browser, independent state)

func (*AnonymousAuthenticator) GetSessionGroup

func (a *AnonymousAuthenticator) GetSessionGroup(r *http.Request, userID string) (string, error)

GetSessionGroup returns a browser-based session group ID.

If the "livetemplate-id" cookie exists, returns its value (persistent groupID). If no cookie exists, generates a new random groupID.

The cookie is set by the handler when a new groupID is generated, ensuring it persists across requests and browser restarts.

func (*AnonymousAuthenticator) Identify

func (a *AnonymousAuthenticator) Identify(r *http.Request) (string, error)

Identify always returns empty string for anonymous users.

type Authenticator

type Authenticator interface {
	// Identify returns the user ID from the request.
	// Returns "" for anonymous users.
	// Returns error if authentication fails (e.g., invalid credentials).
	Identify(r *http.Request) (userID string, err error)

	// GetSessionGroup returns the session group ID for this user.
	// Multiple requests with the same groupID share state.
	//
	// For anonymous users: typically returns a browser-based identifier.
	// For authenticated users: typically returns userID.
	//
	// The groupID determines which Stores instance is used from SessionStore.
	GetSessionGroup(r *http.Request, userID string) (groupID string, err error)
}

Authenticator identifies users and maps them to session groups.

Session groups are the fundamental concept for state sharing: all connections with the same groupID share the same Stores instance. Different groupIDs have completely isolated state.

The Authenticator is called for both HTTP and WebSocket requests to determine: 1. Who is the user? (userID) - can be "" for anonymous 2. Which session group should they join? (groupID)

For most applications, groupID = userID (simple 1:1 mapping), but advanced scenarios can implement custom mappings (e.g., collaborative workspaces where multiple users share one groupID).

type BasicAuthenticator

type BasicAuthenticator struct {
	// ValidateFunc is called to verify username/password credentials.
	// Returns true if credentials are valid, false otherwise.
	// Returns error for system failures (e.g., database connection error).
	ValidateFunc func(username, password string) (bool, error)

	// Realm is the protection space identifier in WWW-Authenticate headers.
	// Default: "LiveTemplate"
	Realm string
}

BasicAuthenticator provides username/password authentication.

This is a helper for integrating with existing authentication systems. It calls a user-provided validation function and maps authenticated users to session groups using a simple 1:1 mapping (groupID = userID).

Security Warnings

HTTPS REQUIRED: BasicAuthenticator uses HTTP Basic Authentication, which sends credentials as base64-encoded strings. This is NOT encrypted and MUST only be used over HTTPS connections. Using HTTP Basic Auth over plain HTTP exposes credentials to network eavesdropping.

BRUTE FORCE PROTECTION: This implementation has no built-in rate limiting or account lockout. For production use, you MUST implement protection against brute force attacks through one or more of:

  • Rate limiting middleware (e.g., golang.org/x/time/rate)
  • Account lockout after N failed attempts
  • External protection (e.g., fail2ban, CloudFlare)
  • Web Application Firewall (WAF) rules

Example usage:

auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
    // Integrate with your authentication system
    return db.ValidateUser(username, password)
})

tmpl := livetemplate.New("app", livetemplate.WithAuthenticator(auth))

For production use, consider implementing a custom Authenticator with: - JWT tokens - OAuth - Session cookies from existing auth middleware - Custom session group mapping logic - Built-in rate limiting and brute force protection

func NewBasicAuthenticator

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

NewBasicAuthenticator creates a BasicAuthenticator with the given validation function.

func (*BasicAuthenticator) GetSessionGroup

func (a *BasicAuthenticator) GetSessionGroup(r *http.Request, userID string) (string, error)

GetSessionGroup returns userID as the session group ID (1:1 mapping).

Each authenticated user gets their own isolated session group. Multiple tabs for the same user share state. Different users have completely isolated state.

Example:

  • User "alice" in Tab 1 → groupID = "alice"
  • User "alice" in Tab 2 → groupID = "alice" (shares state with Tab 1)
  • User "bob" in Tab 1 → groupID = "bob" (isolated from alice)

func (*BasicAuthenticator) Identify

func (a *BasicAuthenticator) Identify(r *http.Request) (string, error)

Identify extracts and validates HTTP Basic Auth credentials.

Returns the username if credentials are valid. Returns error if: - No Authorization header present - Invalid Basic Auth format - Credentials validation fails - System error during validation

func (*BasicAuthenticator) WWWAuthenticate added in v0.8.8

func (a *BasicAuthenticator) WWWAuthenticate() string

WWWAuthenticate returns the WWW-Authenticate header value for HTTP Basic Auth.

type ChallengeAuthenticator added in v0.8.8

type ChallengeAuthenticator interface {
	// WWWAuthenticate returns the value for the WWW-Authenticate response header.
	// Example: `Basic realm="LiveTemplate"`
	WWWAuthenticate() string
}

ChallengeAuthenticator is an optional interface for authenticators that require the browser to prompt for credentials (e.g., HTTP Basic Auth). When Identify returns an error, the handler checks if the authenticator implements this interface and sets the WWW-Authenticate header per RFC 7235.

type Config

type Config struct {
	Upgrader               WSUpgrader
	SessionStore           SessionStore
	Authenticator          Authenticator      // User authentication and session grouping
	PubSubBroadcaster      pubsub.Broadcaster // Optional: for distributed broadcasting across instances
	AllowedOrigins         []string           // Allowed WebSocket origins (empty = allow all in dev, restrict in prod)
	WebSocketDisabled      bool
	LoadingDisabled        bool                                // Disables automatic loading indicator on page load
	TemplateFiles          []string                            // If set, overrides auto-discovery
	TemplateBaseDir        string                              // Base directory for template auto-discovery (default: directory of calling code via runtime.Caller)
	IgnoreTemplateDirs     []string                            // Additional directories to ignore during auto-discovery
	DevMode                bool                                // Development mode - use local client library instead of CDN
	MaxConnections         int64                               // Maximum total connections (0 = unlimited)
	MaxConnectionsPerGroup int64                               // Maximum connections per group (0 = unlimited)
	MessageRateLimit       float64                             // Messages per second per connection (0 = unlimited, default 10)
	MessageRateBurst       int                                 // Burst capacity for rate limiting (default 20)
	CookieMaxAge           time.Duration                       // Session cookie max age (default: 1 year)
	UploadConfigs          map[string]uploadtypes.UploadConfig // Upload field configurations
	WebSocketBufferSize    int                                 // WebSocket send buffer size per connection (default: 50)
	ComponentTemplates     []*TemplateSet                      // Component library templates (parsed before project templates)
	ProgressiveEnhancement bool                                // Enable non-JS form submission support with PRG pattern (default: true)
	TrustForwardedHeaders  bool                                // Trust X-Forwarded-Proto header for scheme detection (default: true)
	DispatchBufferSize     int                                 // Broadcast dispatch channel buffer per connection (default: 16)
}

Config holds template configuration options

type Context added in v0.7.0

type Context struct {
	context.Context
	// contains filtered or unexported fields
}

Context provides unified context for all controller lifecycle methods. It embeds context.Context for cancellation, timeout, and request-scoped values.

Context replaces ActionContext with a single type used across: - Mount(state, ctx) - session initialization - OnConnect(state, ctx) - WebSocket connect - Action methods(state, ctx) - user interactions

func NewContext added in v0.7.0

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

NewContext creates a new Context for action handling.

func (*Context) Action added in v0.7.0

func (c *Context) Action() string

Action returns the action name that triggered this context.

func (*Context) Bind added in v0.7.0

func (c *Context) Bind(v interface{}) error

Bind unmarshals the action data into a struct.

func (*Context) BindAndValidate added in v0.7.0

func (c *Context) BindAndValidate(v interface{}, validate *validator.Validate) error

BindAndValidate binds data to struct and validates it in one step. Uses the provided go-playground/validator instance for validation.

func (*Context) BroadcastAction added in v0.8.8

func (c *Context) BroadcastAction(action string, data map[string]interface{})

func (*Context) DeleteCookie added in v0.7.0

func (c *Context) DeleteCookie(name string) error

DeleteCookie removes an HTTP cookie by setting MaxAge to -1. Returns ErrNoHTTPContext if called from a WebSocket action.

func (*Context) Get added in v0.7.0

func (c *Context) Get(key string) interface{}

func (*Context) GetBool added in v0.7.0

func (c *Context) GetBool(key string) bool

func (*Context) GetCompletedUploads added in v0.7.0

func (c *Context) GetCompletedUploads(name string) []*uploadtypes.UploadEntry

GetCompletedUploads returns all completed upload entries for the given field name.

func (*Context) GetCookie added in v0.7.0

func (c *Context) GetCookie(name string) (*http.Cookie, error)

GetCookie retrieves an HTTP cookie from the request. Returns ErrNoHTTPContext if called from a WebSocket action.

func (*Context) GetFloat added in v0.7.0

func (c *Context) GetFloat(key string) float64

func (*Context) GetInt added in v0.7.0

func (c *Context) GetInt(key string) int

func (*Context) GetString added in v0.7.0

func (c *Context) GetString(key string) string

func (*Context) Has added in v0.7.0

func (c *Context) Has(key string) bool

func (*Context) HasUploads added in v0.7.0

func (c *Context) HasUploads(name string) bool

HasUploads checks if there are any uploads for the given field name.

func (*Context) IsHTTP added in v0.7.0

func (c *Context) IsHTTP() bool

func (*Context) Redirect added in v0.7.0

func (c *Context) Redirect(url string, code int) error

Redirect sends an HTTP redirect response. Returns ErrNoHTTPContext if called from a WebSocket action. Returns ErrInvalidRedirectCode if code is not 3xx. Returns ErrInvalidRedirectURL if URL is not a valid relative path.

func (*Context) Session added in v0.7.0

func (c *Context) Session() Session

Session returns the Session for server-initiated actions.

func (*Context) SetCookie added in v0.7.0

func (c *Context) SetCookie(cookie *http.Cookie) error

SetCookie sets an HTTP cookie on the response. Returns ErrNoHTTPContext if called from a WebSocket action.

func (*Context) SetFlash added in v0.7.7

func (c *Context) SetFlash(key, message string)

SetFlash sets a flash message that will be available in templates via .lvt.Flash(key). Flash messages are page-level notifications (success, info, warning, error). Unlike field errors, flash messages don't affect ResponseMetadata.Success. Flash messages are cleared after each render, so they appear only once.

Common keys: "success", "error", "info", "warning"

Key conventions:

  • Use simple, lowercase keys (e.g., "success", "error")
  • Avoid keys containing colons or special characters
  • Do not use keys starting with "_flash:" (reserved for internal use)

Example:

ctx.SetFlash("success", "Changes saved successfully!")
ctx.SetFlash("error", "Failed to process your request.")

func (*Context) UserID added in v0.7.0

func (c *Context) UserID() string

UserID returns the authenticated user's ID.

func (*Context) ValidateForm added in v0.8.5

func (c *Context) ValidateForm() error

ValidateForm validates form data against HTML attributes inferred from the template. Uses validation rules extracted from HTML attributes like required, pattern, min, max, minlength, maxlength, and input type (email, url, number). Returns MultiError with field-level errors, or nil if all fields are valid.

Note: the schema must be set via WithFormSchema(ExtractFormSchema(statics)). If no schema is set, returns nil (no validation). For production validation with complex rules, use BindAndValidate() with go-playground/validator tags.

Known limitation: ExtractFormSchema merges all forms in a template into one schema. If your template has multiple forms, use BindAndValidate() instead.

func (*Context) WithAction added in v0.7.0

func (c *Context) WithAction(action string) *Context

WithAction returns a new Context with the given action name.

func (*Context) WithData added in v0.7.0

func (c *Context) WithData(data map[string]interface{}) *Context

WithData returns a new Context with the given data.

func (*Context) WithFlashSetter added in v0.7.7

func (c *Context) WithFlashSetter(setter FlashSetter) *Context

WithFlashSetter returns a new Context with the given flash setter.

func (*Context) WithFormSchema added in v0.8.5

func (c *Context) WithFormSchema(schema *FormSchema) *Context

WithFormSchema returns a new Context with the given form validation schema.

func (*Context) WithHTTP added in v0.7.0

func (c *Context) WithHTTP(w http.ResponseWriter, r *http.Request) *Context

WithHTTP returns a new Context with HTTP request/response.

func (*Context) WithSession added in v0.7.0

func (c *Context) WithSession(session Session) *Context

WithSession returns a new Context with the given session.

func (*Context) WithUploads added in v0.7.0

func (c *Context) WithUploads(uploads UploadAccessor) *Context

WithUploads returns a new Context with the given upload accessor.

func (*Context) WithUserID added in v0.7.0

func (c *Context) WithUserID(userID string) *Context

WithUserID returns a new Context with the given user ID.

type DispatchError added in v0.5.2

type DispatchError struct {
	Action    string
	StoreType string
	Err       error
}

DispatchError provides context about a failed dispatch.

func (*DispatchError) Error added in v0.5.2

func (e *DispatchError) Error() string

func (*DispatchError) Unwrap added in v0.5.2

func (e *DispatchError) Unwrap() error

type EnvConfig

type EnvConfig struct {
	// MaxConnections is the maximum number of concurrent WebSocket connections.
	// 0 means unlimited (default).
	// Environment: LVT_MAX_CONNECTIONS
	MaxConnections int64

	// MaxConnectionsPerGroup is the maximum connections per session group.
	// 0 means unlimited (default). Prevents single users from exhausting limits.
	// Environment: LVT_MAX_CONNECTIONS_PER_GROUP
	MaxConnectionsPerGroup int64

	// AllowedOrigins is a comma-separated list of allowed WebSocket origins.
	// Empty means allow all in dev mode, restrict in production.
	// Environment: LVT_ALLOWED_ORIGINS
	// Example: "https://example.com,https://app.example.com"
	AllowedOrigins []string

	// DevMode enables development mode features.
	// - Uses local client library instead of CDN
	// - More verbose logging
	// Environment: LVT_DEV_MODE (true/false, 1/0)
	DevMode bool

	// WebSocketDisabled disables WebSocket connections (HTTP-only mode).
	// Environment: LVT_WEBSOCKET_DISABLED (true/false, 1/0)
	WebSocketDisabled bool

	// LoadingDisabled disables the automatic loading indicator.
	// Environment: LVT_LOADING_DISABLED (true/false, 1/0)
	LoadingDisabled bool

	// TemplateBaseDir is the base directory for template auto-discovery.
	// Empty means use runtime.Caller detection (default).
	// Environment: LVT_TEMPLATE_BASE_DIR
	// Example: "/app/templates", "./templates", "."
	TemplateBaseDir string

	// ShutdownTimeout is the maximum duration to wait for graceful shutdown.
	// Default: 30 seconds
	// Environment: LVT_SHUTDOWN_TIMEOUT
	// Example: "30s", "1m", "500ms"
	// Note: Reserved for future use. Currently loaded and validated but not applied.
	ShutdownTimeout time.Duration

	// LogLevel sets the logging level (debug, info, warn, error).
	// Default: "info"
	// Environment: LVT_LOG_LEVEL
	// Note: Reserved for future use. Currently loaded and validated but not applied.
	LogLevel string

	// MetricsEnabled enables Prometheus metrics export.
	// Default: true
	// Environment: LVT_METRICS_ENABLED (true/false, 1/0)
	// Note: Reserved for future use. Currently loaded and validated but not applied.
	MetricsEnabled bool

	// ProgressiveEnhancement enables non-JS form submission support.
	// When enabled (default: true), HTTP form submissions from non-JavaScript clients
	// receive full HTML page responses using the POST-Redirect-GET pattern.
	// Environment: LVT_PROGRESSIVE_ENHANCEMENT (true/false, 1/0)
	ProgressiveEnhancement bool

	// WebSocketBufferSize sets the send buffer size per WebSocket connection.
	// Controls backpressure behavior: slow clients are disconnected when buffer is full.
	// Environment: LVT_WS_BUFFER_SIZE (positive integer, default: 50)
	WebSocketBufferSize int

	// TrustForwardedHeaders controls whether X-Forwarded-Proto is trusted for
	// scheme detection in same-origin WebSocket checks.
	// Default: true (safe when behind a reverse proxy).
	// Set to false if the server is directly reachable by clients without a proxy.
	// Environment: LVT_TRUST_FORWARDED_HEADERS (true/false, 1/0)
	TrustForwardedHeaders bool
}

EnvConfig holds environment-based configuration for LiveTemplate.

All configuration can be set via environment variables with the LVT_ prefix. This follows the 12-factor app methodology for configuration management.

func LoadEnvConfig

func LoadEnvConfig() (*EnvConfig, error)

LoadEnvConfig loads configuration from environment variables.

All environment variables are prefixed with LVT_ (LiveTemplate). Boolean values can be "true"/"false" or "1"/"0". Duration values use Go duration format (e.g., "30s", "1m").

Example:

export LVT_MAX_CONNECTIONS=10000
export LVT_ALLOWED_ORIGINS="https://example.com,https://app.example.com"
export LVT_DEV_MODE=false
export LVT_SHUTDOWN_TIMEOUT=30s
config := livetemplate.LoadEnvConfig()

func (*EnvConfig) ToOptions

func (c *EnvConfig) ToOptions() []Option

ToOptions converts EnvConfig to a slice of Option functions.

This allows using environment-based configuration with the existing Option-based API.

Example:

envConfig, err := livetemplate.LoadEnvConfig()
if err != nil {
    log.Fatal(err)
}
tmpl := livetemplate.New("app", envConfig.ToOptions()...)

func (*EnvConfig) Validate

func (c *EnvConfig) Validate() error

Validate checks that the configuration is valid.

Returns an error if any configuration value is invalid.

type FieldError

type FieldError struct {
	Field   string
	Message string
}

FieldError represents a validation error for a specific field

func NewFieldError

func NewFieldError(field string, err error) FieldError

NewFieldError creates a field-specific error

func (FieldError) Error

func (e FieldError) Error() string

type FlashSetter added in v0.7.7

type FlashSetter interface {
	// contains filtered or unexported methods
}

FlashSetter allows setting flash messages from action handlers. Flash messages are page-level notifications (success, info, warning, error) that don't affect ResponseMetadata.Success (unlike field validation errors).

The setFlash method is intentionally unexported to ensure flash messages are only set through the Context.SetFlash() public API, maintaining consistent behavior and preventing direct message map manipulation.

type FormRule added in v0.8.5

type FormRule struct {
	Field     string
	Required  bool
	InputType string // "email", "url", "number", "tel"
	MinLength int    // -1 if not set
	MaxLength int    // -1 if not set
	Min       float64
	Max       float64
	HasMin    bool
	HasMax    bool
	Pattern   string         // raw pattern string
	PatternRe *regexp.Regexp // pre-compiled pattern (nil if invalid or absent)
}

FormRule represents a validation rule inferred from HTML input attributes.

type FormSchema added in v0.8.5

type FormSchema struct {
	Rules []FormRule
}

FormSchema holds validation rules inferred from template statics.

func ExtractFormSchema added in v0.8.5

func ExtractFormSchema(statics []string) *FormSchema

ExtractFormSchema scans template statics for HTML validation attributes on <input>, <textarea>, and <select> elements.

Known limitation: if a field's name attribute is a template expression (dynamic), it will be split across statics and may not be detected.

func (*FormSchema) Validate added in v0.8.5

func (s *FormSchema) Validate(data map[string]interface{}) error

Validate checks form data against the schema rules. Returns MultiError with field-level errors, or nil if valid.

type GorillaOption added in v0.8.5

type GorillaOption func(*websocket.Upgrader)

GorillaOption configures the gorilla WebSocket upgrader.

func WithGorillaCheckOrigin added in v0.8.5

func WithGorillaCheckOrigin(fn func(*http.Request) bool) GorillaOption

WithGorillaCheckOrigin sets the origin check function for the gorilla upgrader.

func WithGorillaCompression added in v0.8.5

func WithGorillaCompression() GorillaOption

WithGorillaCompression enables permessage-deflate compression. Reduces bandwidth for larger payloads at the cost of CPU.

func WithGorillaReadBufferSize added in v0.8.5

func WithGorillaReadBufferSize(size int) GorillaOption

WithGorillaReadBufferSize sets the read buffer size for the gorilla upgrader.

func WithGorillaWriteBufferPool added in v0.8.5

func WithGorillaWriteBufferPool(pool websocket.BufferPool) GorillaOption

WithGorillaWriteBufferPool sets a custom write buffer pool. Pass nil to disable pooling (each connection allocates its own buffer).

func WithGorillaWriteBufferSize added in v0.8.5

func WithGorillaWriteBufferSize(size int) GorillaOption

WithGorillaWriteBufferSize sets the write buffer size for the gorilla upgrader.

type GorillaUpgrader added in v0.8.5

type GorillaUpgrader struct {
	// contains filtered or unexported fields
}

GorillaUpgrader wraps gorilla/websocket.Upgrader as a WSUpgrader.

func NewGorillaUpgrader added in v0.8.5

func NewGorillaUpgrader(opts ...GorillaOption) *GorillaUpgrader

NewGorillaUpgrader creates a WSUpgrader backed by gorilla/websocket. Default buffer sizes are 1024 bytes (optimized for LiveTemplate's small payloads). Write buffers are pooled via sync.Pool to avoid per-connection allocation.

func (*GorillaUpgrader) Copy added in v0.8.5

func (g *GorillaUpgrader) Copy() *GorillaUpgrader

Copy creates a shallow copy of the upgrader to avoid mutating shared state.

func (*GorillaUpgrader) SetCheckOrigin added in v0.8.5

func (g *GorillaUpgrader) SetCheckOrigin(fn func(*http.Request) bool)

SetCheckOrigin sets the origin check function on the underlying gorilla upgrader.

func (*GorillaUpgrader) SetCompression added in v0.8.5

func (g *GorillaUpgrader) SetCompression(enabled bool)

SetCompression enables or disables permessage-deflate compression.

func (*GorillaUpgrader) Upgrade added in v0.8.5

func (g *GorillaUpgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (WSConn, error)

Upgrade upgrades an HTTP connection to a WebSocket connection.

type HandleOption added in v0.7.0

type HandleOption func(*handleConfig)

HandleOption configures Handle behavior

func WithStore added in v0.7.0

func WithStore(store SessionStore) HandleOption

WithStore sets the session store for state persistence. Use this to configure Redis or other distributed stores.

type HealthChecker

type HealthChecker interface {
	// Check performs a health check and returns an error if unhealthy.
	// The context may have a timeout, so checks should respect it.
	Check(ctx context.Context) error
}

HealthChecker represents a component that can be health-checked.

Implementations should complete the check quickly (<100ms) to avoid blocking Kubernetes probes or load balancer health checks.

type HealthHandler

type HealthHandler struct {
	// contains filtered or unexported fields
}

HealthHandler provides HTTP endpoints for health checks.

Supports two types of health checks:

  1. Liveness (/health/live): Process is running and not deadlocked
  2. Readiness (/health/ready): Process can handle requests (dependencies healthy)

Kubernetes usage:

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

func NewHealthHandler

func NewHealthHandler(timeout time.Duration) *HealthHandler

NewHealthHandler creates a new health check handler.

The timeout parameter specifies the maximum duration for all checks. If timeout is 0, a default of 5 seconds is used.

func (*HealthHandler) Live

func (h *HealthHandler) Live(w http.ResponseWriter, r *http.Request)

Live handles the liveness probe endpoint.

Returns 200 OK if the process is alive and not deadlocked. This endpoint should always succeed unless the process is completely broken.

Example: GET /health/live Response: 200 OK with {"status":"healthy","timestamp":"2025-11-02T08:00:00Z"}

func (*HealthHandler) Ready

func (h *HealthHandler) Ready(w http.ResponseWriter, r *http.Request)

Ready handles the readiness probe endpoint.

Returns 200 OK if all registered health checkers pass. Returns 503 Service Unavailable if any checker fails.

Kubernetes will not route traffic to pods that fail readiness checks.

Example: GET /health/ready Response: 200 OK with detailed check results

func (*HealthHandler) RegisterChecker

func (h *HealthHandler) RegisterChecker(name string, checker HealthChecker)

RegisterChecker adds a health checker for a named component.

The name should be descriptive (e.g., "database", "redis", "session-store"). Thread-safe: can be called concurrently.

type HealthResponse

type HealthResponse struct {
	Status    string         `json:"status"`               // "healthy" or "unhealthy"
	Timestamp string         `json:"timestamp"`            // ISO 8601 timestamp
	Checks    []HealthStatus `json:"checks,omitempty"`     // Individual component statuses
	TotalTime string         `json:"total_time,omitempty"` // Total check duration
}

HealthResponse represents the overall health check response.

type HealthStatus

type HealthStatus struct {
	Name   string `json:"name"`            // Component name
	Status string `json:"status"`          // "healthy" or "unhealthy"
	Error  string `json:"error,omitempty"` // Error message if unhealthy
}

HealthStatus represents the health status of a component.

type LiveHandler

type LiveHandler interface {
	http.Handler

	// Shutdown gracefully shuts down the handler, draining connections.
	//
	// It performs the following steps:
	//  1. Stops accepting new WebSocket connections
	//  2. Sends close frames to all active WebSocket connections
	//  3. Waits for in-flight requests to complete (respecting context timeout)
	//
	// The context timeout controls how long to wait for connections to close.
	// After the timeout, remaining connections are forcefully closed.
	//
	// Example usage with http.Server:
	//   ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	//   defer cancel()
	//   handler.Shutdown(ctx)
	//   server.Shutdown(ctx)
	Shutdown(ctx context.Context) error

	// MetricsHandler returns an http.Handler that exports Prometheus metrics.
	//
	// The handler responds to GET requests with metrics in Prometheus text format.
	// Typically mounted at /metrics for scraping by Prometheus.
	//
	// Example with standard library http mux:
	//   mux := http.NewServeMux()
	//   handler := template.Handle(store)
	//   mux.Handle("/live", handler)
	//   mux.Handle("/metrics", handler.MetricsHandler())
	//   http.ListenAndServe(":8080", mux)
	//
	// Example with gorilla/mux:
	//   r := mux.NewRouter()
	//   handler := template.Handle(store)
	//   r.Handle("/live", handler)
	//   r.Handle("/metrics", handler.MetricsHandler())
	MetricsHandler() http.Handler
}

LiveHandler is the interface returned by Template.Handle() It provides HTTP handling and lifecycle management for live template connections.

For server-initiated actions, implement an OnConnect(state, ctx) lifecycle method on your controller and call ctx.Session() to obtain a Session handle that can be used to trigger actions from background goroutines. See the Session interface above and docs/references/server-actions.md for details.

type MemorySessionStore

type MemorySessionStore struct {
	// contains filtered or unexported fields
}

MemorySessionStore is an in-memory session store with automatic cleanup.

Features: - Thread-safe for concurrent access - Tracks last access time for each group - Automatic cleanup of inactive groups (configurable TTL) - Suitable for single-instance deployments

For multi-instance deployments, use a persistent SessionStore (e.g., Redis).

func NewMemorySessionStore

func NewMemorySessionStore(opts ...SessionStoreOption) *MemorySessionStore

NewMemorySessionStore creates a new in-memory session store with automatic cleanup.

Default configuration: - Cleanup TTL: 24 hours - Cleanup interval: 1 hour

The cleanup goroutine runs in the background and removes session groups that haven't been accessed within the TTL period. This prevents memory leaks from abandoned sessions.

Call Close() to stop the cleanup goroutine when shutting down.

func (*MemorySessionStore) Close

func (s *MemorySessionStore) Close()

Close stops the cleanup goroutine. Should be called when shutting down the application.

func (*MemorySessionStore) Delete

func (s *MemorySessionStore) Delete(ctx context.Context, groupID string)

Delete removes a session group and all its state. The context parameter is accepted for interface compliance but not used for in-memory operations.

func (*MemorySessionStore) Get

func (s *MemorySessionStore) Get(ctx context.Context, groupID string) interface{}

Get retrieves the state for a session group. Updates the last access time for the group. The context parameter is accepted for interface compliance but not used for in-memory operations.

func (*MemorySessionStore) List

func (s *MemorySessionStore) List(ctx context.Context) []string

List returns all active session group IDs. The context parameter is accepted for interface compliance but not used for in-memory operations.

func (*MemorySessionStore) Set

func (s *MemorySessionStore) Set(ctx context.Context, groupID string, state interface{})

Set stores state for a session group. Updates the last access time for the group. The context parameter is accepted for interface compliance but not used for in-memory operations.

type MultiError

type MultiError []FieldError

MultiError is a collection of field errors (implements error interface)

func ValidationToMultiError

func ValidationToMultiError(err error) MultiError

ValidationToMultiError converts go-playground/validator errors to MultiError

func (MultiError) Error

func (m MultiError) Error() string

type Option

type Option func(*Config)

Option is a functional option for configuring a Template

func WithAllowedOrigins

func WithAllowedOrigins(origins []string) Option

WithAllowedOrigins sets the allowed WebSocket origins for CORS protection.

When set, WebSocket upgrade requests will be validated against this list. Requests from origins not in the list will be rejected with 403 Forbidden.

If empty (default):

  • Development: All origins allowed (permissive for local dev)
  • Production: Consider setting explicitly for security

Example for production:

tmpl := livetemplate.New("app",
    livetemplate.WithAllowedOrigins([]string{
        "https://yourdomain.com",
        "https://www.yourdomain.com",
    }))

Security note: Always set this in production to prevent CSRF attacks via WebSocket.

When AllowedOrigins is not set, same-origin detection relies on X-Forwarded-Proto (if present and trusted) or r.TLS to determine the request scheme. By default, X-Forwarded-Proto is trusted (see WithTrustForwardedHeaders). If the server is directly reachable by clients without a proxy, either set WithTrustForwardedHeaders(false) to ignore forwarded headers, or use WithAllowedOrigins to explicitly list trusted origins.

func WithAuthenticator

func WithAuthenticator(auth Authenticator) Option

WithAuthenticator sets a custom authenticator for user identification and session grouping.

The authenticator determines:

  • Who is the user? (userID via Identify)
  • Which session group should they join? (groupID via GetSessionGroup)

Default: AnonymousAuthenticator (browser-based session grouping)

Example with BasicAuthenticator:

auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
    return db.ValidateUser(username, password)
})
tmpl := livetemplate.New("app", livetemplate.WithAuthenticator(auth))

Example with custom JWT authenticator:

tmpl := livetemplate.New("app", livetemplate.WithAuthenticator(myJWTAuth))

func WithComponentTemplates added in v0.7.1

func WithComponentTemplates(sets ...*TemplateSet) Option

WithComponentTemplates registers component library templates to be parsed before project templates. This enables using pre-built UI components from the livetemplate/components library or custom component libraries.

Component templates are parsed first, then project templates are parsed on top, allowing project templates to override component templates with the same name.

Example:

import "github.com/livetemplate/lvt/components"

tmpl, err := livetemplate.New("app",
    livetemplate.WithComponentTemplates(components.All()...),
)

Or with specific components:

import (
    "github.com/livetemplate/lvt/components/dropdown"
    "github.com/livetemplate/lvt/components/tabs"
)

tmpl, err := livetemplate.New("app",
    livetemplate.WithComponentTemplates(
        dropdown.Templates(),
        tabs.Templates(),
    ),
)

Templates are parsed in the order provided. Official component templates use the naming convention "lvt:<category>:<name>:v<version>" (e.g., "lvt:dropdown:searchable:v1"). Third-party components may use their own prefix (e.g., "myorg:widget:default:v1").

func WithCookieMaxAge added in v0.1.3

func WithCookieMaxAge(maxAge time.Duration) Option

WithCookieMaxAge sets the maximum age for session cookies.

The cookie is used to maintain anonymous user sessions across page reloads. Default: 365 days (1 year)

Example:

tmpl := livetemplate.New("app",
    livetemplate.WithCookieMaxAge(30*24*time.Hour), // 30 days
)

func WithDevMode

func WithDevMode(enabled bool) Option

WithDevMode enables development mode - uses local client library instead of CDN

func WithDispatchBufferSize added in v0.8.8

func WithDispatchBufferSize(size int) Option

WithDispatchBufferSize sets the buffer size for the broadcast dispatch channel per WebSocket connection. This is separate from the WebSocket send buffer (WithWebSocketBufferSize) because dispatch requests are less frequent. Default: 16. Increase for apps with high broadcast fan-out.

func WithIgnoreTemplateDirs

func WithIgnoreTemplateDirs(dirs ...string) Option

WithIgnoreTemplateDirs adds directories to ignore during template auto-discovery. This is useful to skip directories containing generator templates or other non-runtime templates.

Example:

tmpl := livetemplate.New("app", livetemplate.WithIgnoreTemplateDirs("generators", "scaffolds"))

func WithLoadingDisabled

func WithLoadingDisabled() Option

WithLoadingDisabled disables the automatic loading indicator shown during page initialization

func WithMaxConnections

func WithMaxConnections(max int64) Option

WithMaxConnections sets the maximum number of concurrent connections. 0 (default) means unlimited.

func WithMaxConnectionsPerGroup

func WithMaxConnectionsPerGroup(max int64) Option

WithMaxConnectionsPerGroup sets the maximum number of connections per session group. 0 (default) means unlimited. Prevents single users from exhausting connection limits.

func WithMessageRateLimit added in v0.1.3

func WithMessageRateLimit(messagesPerSecond float64, burstCapacity int) Option

WithMessageRateLimit sets the rate limit for WebSocket messages per connection.

Uses token bucket algorithm: messagesPerSecond determines the rate, burstCapacity allows short bursts above the rate.

Default: 10 messages/sec with burst of 20. Set messagesPerSecond = 0 to disable rate limiting (not recommended for production).

Example:

tmpl := livetemplate.New("app",
    livetemplate.WithMessageRateLimit(20, 50), // 20 msg/sec, burst of 50
)

func WithParseFiles

func WithParseFiles(files ...string) Option

WithParseFiles specifies template files to parse, overriding auto-discovery

func WithPermissiveOriginCheck added in v0.1.3

func WithPermissiveOriginCheck() Option

WithPermissiveOriginCheck disables origin checking for WebSocket connections.

WARNING: This allows connections from any origin and should ONLY be used in:

  • Local development environments
  • Testing scenarios
  • Specific use cases where CSRF protection is handled externally

In production, use WithAllowedOrigins() instead to specify trusted origins.

Example:

// Development only - DO NOT use in production
tmpl := livetemplate.New("app",
    livetemplate.WithDevMode(true),
    livetemplate.WithPermissiveOriginCheck(),
)

func WithProgressiveEnhancement added in v0.8.1

func WithProgressiveEnhancement(enabled bool) Option

WithProgressiveEnhancement enables or disables progressive enhancement support.

When enabled (default: true), HTTP form submissions from non-JavaScript clients receive full HTML page responses instead of JSON. This allows applications to work without JavaScript using standard HTML form submissions.

The feature uses the POST-Redirect-GET (PRG) pattern:

  • Successful actions: 303 redirect to prevent duplicate submissions on refresh
  • Validation errors: Re-render page with errors inline (no redirect)

Detection uses the Accept header: clients sending "application/json" receive JSON, while browsers sending "text/html" receive full HTML pages.

Example form structure for progressive enhancement:

<form method="POST">
    <input type="text" name="title">
    <button name="action" value="add" type="submit">Add</button>
</form>

Using an explicit action value avoids ambiguous POST parsing when other form fields submit empty strings.

The form works with both JavaScript (via WebSocket or fetch/JSON) and without JavaScript (via method="POST").

func WithPubSubBroadcaster

func WithPubSubBroadcaster(broadcaster pubsub.Broadcaster) Option

WithPubSubBroadcaster enables distributed broadcasting across multiple application instances.

When set, Broadcast*, BroadcastToUsers, and BroadcastToGroup methods will publish messages to Redis Pub/Sub for distribution to all instances. Each instance subscribes to these messages and fans them out to its local connections.

This is essential for horizontal scaling - without it, broadcasts only reach connections on the same instance.

Example:

import (
    "github.com/livetemplate/livetemplate"
    "github.com/livetemplate/livetemplate/pubsub"
    "github.com/redis/go-redis/v9"
)

redisClient := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

broadcaster := pubsub.NewRedisBroadcaster(redisClient)

tmpl := livetemplate.New("app",
    livetemplate.WithPubSubBroadcaster(broadcaster),
)

func WithSessionStore

func WithSessionStore(store SessionStore) Option

WithSessionStore sets a custom session store for HTTP requests

func WithTemplateBaseDir added in v0.2.1

func WithTemplateBaseDir(dir string) Option

WithTemplateBaseDir sets the base directory for template auto-discovery. This overrides the default runtime.Caller detection. Useful when running via 'go run' or when templates are in a non-standard location.

func WithTrustForwardedHeaders added in v0.8.5

func WithTrustForwardedHeaders(trust bool) Option

WithTrustForwardedHeaders controls whether X-Forwarded-Proto is trusted for scheme detection in same-origin WebSocket checks.

Default: true (backward compatible). When true, the origin checker reads X-Forwarded-Proto to determine whether the original client connection used HTTP or HTTPS. This is safe when the server is behind a reverse proxy that sets/overwrites this header.

Set to false if the server is directly reachable by clients (no proxy) to prevent clients from forging the header. In this case, scheme detection falls back to r.TLS (non-nil = HTTPS, nil = HTTP).

This option only affects the default same-origin check. It has no effect when WithAllowedOrigins is set (explicit origins take priority).

func WithUpgrader

func WithUpgrader(upgrader WSUpgrader) Option

WithUpgrader sets a custom WebSocket upgrader.

func WithUpload added in v0.3.1

func WithUpload(name string, config uploadtypes.UploadConfig) Option

WithUpload configures file upload support for a specific form field.

Upload configuration specifies validation rules, size limits, and storage options. Once configured, uploads are accessible via Context during action handling.

Example:

tmpl := livetemplate.New("profile",
    livetemplate.WithUpload("avatar", livetemplate.UploadConfig{
        Accept:      []string{"image/png", "image/jpeg"},
        MaxFileSize: 5 << 20, // 5 MB
        MaxFiles:    1,
    }),
)

In your controller's action method, access uploads via Context:

func (c *ProfileController) SaveProfile(state ProfileState, ctx *livetemplate.Context) (ProfileState, error) {
    if ctx.HasUploads("avatar") {
        for _, entry := range ctx.GetCompletedUploads("avatar") {
            state.AvatarURL = moveToStorage(entry.TempPath)
        }
    }
    return state, nil
}

func WithWebSocketBufferSize added in v0.4.0

func WithWebSocketBufferSize(size int) Option

WithWebSocketBufferSize sets the send buffer size per WebSocket connection.

The buffer queues messages for async delivery. Larger buffers handle burst traffic better but use more memory. Smaller buffers use less memory but may close slow clients more aggressively.

Default: 50 messages per connection

  • Memory per connection: ~50KB (assuming 1KB avg message size)
  • Memory for 100 connections: ~5MB

Recommended values:

  • Low traffic / memory constrained: 10-25
  • Normal traffic: 50 (default)
  • High traffic / burst heavy: 100-1000

Environment variable override: LVT_WS_BUFFER_SIZE

Example:

// High-throughput application
tmpl := livetemplate.New("app", livetemplate.WithWebSocketBufferSize(100))

// Memory-constrained environment
tmpl := livetemplate.New("app", livetemplate.WithWebSocketBufferSize(10))

func WithWebSocketCompression added in v0.8.5

func WithWebSocketCompression() Option

WithWebSocketCompression enables permessage-deflate WebSocket compression. Reduces bandwidth for larger payloads at the cost of CPU. Only effective when using the default GorillaUpgrader.

func WithWebSocketDisabled

func WithWebSocketDisabled() Option

WithWebSocketDisabled disables WebSocket support, forcing HTTP-only mode

type Presigner added in v0.3.1

type Presigner = uploadtypes.Presigner

Presigner generates presigned upload URLs for external storage (S3, GCS, etc). This enables direct client-to-storage uploads, bypassing the server.

Implementations should return an error if presigning fails due to:

  • Invalid or expired credentials
  • Network connectivity issues
  • Storage service errors
  • Invalid upload configuration
Example

ExamplePresigner demonstrates implementing a custom presigner.

type CustomPresigner struct {
	endpoint string
	apiKey   string
}

presign := func(p *CustomPresigner, entry *UploadEntry) (UploadMeta, error) {
	return UploadMeta{
		Uploader: "custom",
		URL:      p.endpoint + "/upload/" + entry.ID,
		Headers: map[string]string{
			"Authorization": "Bearer " + p.apiKey,
			"Content-Type":  entry.ClientType,
		},
	}, nil
}

presigner := &CustomPresigner{
	endpoint: "https://storage.example.com",
	apiKey:   "secret-key",
}

entry := &UploadEntry{
	ID:         "entry-123",
	ClientType: "image/jpeg",
}

meta, _ := presign(presigner, entry)
_ = meta.URL // https://storage.example.com/upload/entry-123

type RedisHealthChecker

type RedisHealthChecker struct {
	// contains filtered or unexported fields
}

RedisHealthChecker checks the health of a Redis connection.

func NewRedisHealthChecker

func NewRedisHealthChecker(store *RedisSessionStore) *RedisHealthChecker

NewRedisHealthChecker creates a health checker for Redis.

func (*RedisHealthChecker) Check

func (r *RedisHealthChecker) Check(ctx context.Context) error

Check implements the HealthChecker interface. Uses PingContext to ensure the operation respects context cancellation and timeouts.

type RedisSessionStore

type RedisSessionStore struct {
	// contains filtered or unexported fields
}

RedisSessionStore implements SessionStore using Redis for distributed session management.

Features: - Thread-safe for concurrent access - Automatic TTL refresh on access - Connection retry with exponential backoff - Hash-based storage with JSON serialization (v2 schema) - Automatic migration from legacy Gob-encoded blob format (v1) - Suitable for multi-instance deployments

Redis Key Schema (v2):

  • livetemplate:session:{groupID} -> Redis HASH
  • "_meta" field: JSON metadata (version, updated_at)
  • "{storeName}" fields: JSON-encoded individual stores
  • TTL: 24 hours (configurable)

The v2 schema uses Redis HASH to enable granular updates via HSET, which is more efficient than re-writing the entire session blob.

func NewRedisSessionStore

func NewRedisSessionStore(client redis.UniversalClient, opts ...RedisSessionStoreOption) *RedisSessionStore

NewRedisSessionStore creates a new Redis-backed session store.

The client parameter can be:

  • redis.Client for single-node Redis
  • redis.ClusterClient for Redis Cluster
  • redis.Ring for Redis Ring (sharding)
  • redis.FailoverClient for Redis Sentinel

Example:

client := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})
store := livetemplate.NewRedisSessionStore(client,
    livetemplate.WithSessionTTL(24*time.Hour),
)

func (*RedisSessionStore) Close

func (s *RedisSessionStore) Close() error

Close closes the Redis client connection and stops the refresh worker. Should be called when shutting down the application.

func (*RedisSessionStore) Delete

func (s *RedisSessionStore) Delete(ctx context.Context, groupID string)

Delete removes a session group and all its state. The context is used for Redis operations and can timeout/cancel requests.

func (*RedisSessionStore) Get

func (s *RedisSessionStore) Get(ctx context.Context, groupID string) interface{}

Get retrieves the state for a session group. Returns nil if the group doesn't exist or if deserialization fails. Automatically refreshes the TTL on successful access. The context is used for Redis operations and can timeout/cancel requests.

Note: RedisSessionStore now stores state as JSON-encoded data.

func (*RedisSessionStore) List

func (s *RedisSessionStore) List(ctx context.Context) []string

List returns all active session group IDs. Used for broadcasting and cleanup operations. The context is used for Redis operations and can timeout/cancel requests.

func (*RedisSessionStore) Ping

func (s *RedisSessionStore) Ping() error

Ping checks if the Redis connection is healthy. Used for health check integration.

func (*RedisSessionStore) PingContext added in v0.1.3

func (s *RedisSessionStore) PingContext(ctx context.Context) error

PingContext pings the Redis server with the given context. The context can be used to set timeouts or cancel the operation early.

func (*RedisSessionStore) Set

func (s *RedisSessionStore) Set(ctx context.Context, groupID string, state interface{})

Set stores state for a session group. For the Controller+State pattern, this stores the state as JSON. The context is used for Redis operations and can timeout/cancel requests.

type RedisSessionStoreOption

type RedisSessionStoreOption func(*RedisSessionStore)

RedisSessionStoreOption configures RedisSessionStore

func WithMaxRetries

func WithMaxRetries(maxRetries int) RedisSessionStoreOption

WithMaxRetries sets the maximum number of retry attempts for Redis operations. Default: 3

func WithRetryDelay

func WithRetryDelay(delay time.Duration) RedisSessionStoreOption

WithRetryDelay sets the base delay between retry attempts. Actual delay uses exponential backoff: delay * 2^attempt Default: 100ms

func WithSessionTTL

func WithSessionTTL(ttl time.Duration) RedisSessionStoreOption

WithSessionTTL sets the time-to-live for session groups. Sessions not accessed within this duration will be automatically expired by Redis. Default: 24 hours

type ResponseMetadata

type ResponseMetadata = send.ResponseMetadata

ResponseMetadata contains information about the action that generated the update. This is an alias for internal/send.ResponseMetadata for backward compatibility.

type Session added in v0.5.0

type Session interface {
	// TriggerAction dispatches the action to the matching controller
	// method on every connection in the session group, then sends the
	// updated template to each of those connections.
	//
	// This behaves identically to client-initiated actions: the action
	// runs through the controller's action method, errors are captured,
	// and diffs are sent over WebSocket to each connection.
	//
	// Example:
	//   session.TriggerAction("tick", nil)
	//   session.TriggerAction("new_notification", map[string]interface{}{"id": 123})
	TriggerAction(action string, data map[string]interface{}) error
}

Session allows controllers to trigger server-initiated actions for connected clients. Actions triggered via Session affect every connection in the same session group (all tabs sharing one browser session, plus any additional devices that the configured Authenticator places in the same group).

This is the recommended way to implement:

  • Timers and ticks
  • Background job completion notifications
  • Webhook-triggered updates
  • Cross-tab synchronization

Scope: Session is scoped to a session group (groupID), not to 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 — a user with multiple devices may share a group across devices (by returning a stable groupID keyed on userID) or may have per-device groups (by returning a per-session groupID). Session.TriggerAction always targets the group of the Context it was obtained from, never other groups.

type SessionStore

type SessionStore interface {
	// Get retrieves the state for a session group.
	// Returns nil if the group doesn't exist.
	// The context can be used for cancellation, timeouts, and tracing.
	Get(ctx context.Context, groupID string) interface{}

	// Set stores state for a session group.
	// Creates a new group if it doesn't exist, updates if it does.
	// The context can be used for cancellation, timeouts, and tracing.
	Set(ctx context.Context, groupID string, state interface{})

	// Delete removes a session group and all its state.
	// The context can be used for cancellation, timeouts, and tracing.
	Delete(ctx context.Context, groupID string)

	// List returns all active session group IDs.
	// Used for broadcasting and cleanup operations.
	// The context can be used for cancellation, timeouts, and tracing.
	List(ctx context.Context) []string
}

SessionStore manages session groups, where each group contains Stores shared across connections.

A session group is the fundamental isolation boundary: all connections with the same groupID share the same Stores instance. Different groupIDs have completely isolated state.

For anonymous users: groupID is typically a browser-based identifier (all tabs share state). For authenticated users: groupID is typically the userID (each user has isolated state).

Thread-safety: All implementations must be safe for concurrent access from multiple goroutines.

type SessionStoreHealthChecker

type SessionStoreHealthChecker struct {
	// contains filtered or unexported fields
}

SessionStoreHealthChecker implements HealthChecker for SessionStore.

Checks that the session store is accessible by attempting to get a non-existent session (should return nil without error).

func NewSessionStoreHealthChecker

func NewSessionStoreHealthChecker(store SessionStore) *SessionStoreHealthChecker

NewSessionStoreHealthChecker creates a health checker for a SessionStore.

func (*SessionStoreHealthChecker) Check

Check verifies the session store is accessible by performing a Get/Set/Delete cycle. This properly validates that the store can read, write, and delete data.

type SessionStoreOption

type SessionStoreOption func(*MemorySessionStore)

SessionStoreOption configures MemorySessionStore

func WithCleanupInterval added in v0.1.3

func WithCleanupInterval(interval time.Duration) SessionStoreOption

WithCleanupInterval sets how often the cleanup process runs. Lower intervals = more frequent cleanup but more CPU usage. Higher intervals = less frequent cleanup but potentially more memory usage. Default: 1 hour

func WithCleanupTTL

func WithCleanupTTL(ttl time.Duration) SessionStoreOption

WithCleanupTTL sets the time-to-live for inactive session groups. Groups not accessed within this duration will be automatically cleaned up. Default: 24 hours

type State added in v0.7.0

type State interface {
	encoding.BinaryMarshaler
	encoding.BinaryUnmarshaler
	Inner() any // Returns the underlying value for framework use
}

State is the interface for session state that can be persisted. The serialization requirement ensures state contains only pure data. Use AsState[T]() for zero-boilerplate implementation.

Controllers hold dependencies (DB, Logger, etc.) and are never cloned. State holds pure data and is cloned per session via serialization.

func AsState added in v0.7.0

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

AsState wraps a plain struct pointer to satisfy the State interface. Panics if the state type contains dependency fields (e.g., *sql.DB, *slog.Logger) that belong in the controller. Checks direct fields, nested structs, pointer-to-struct fields, and slice/array/map element types. Uses JSON serialization by default. For custom serialization, implement the State interface directly on your type.

The check is best-effort: it matches a fixed set of known stdlib dependency types. Custom wrappers (e.g., type AppDB struct{ *sql.DB }) or third-party types (e.g., *pgxpool.Pool) are not caught. Use AssertPureState[T]() in tests for stricter validation.

Example:

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

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template represents a live template with caching and tree-based optimization capabilities. It provides an API similar to html/template.Template but with additional ExecuteUpdates method for generating tree-based updates that can be efficiently transmitted to clients.

func Must added in v0.3.0

func Must(t *Template, err error) *Template

Must is a helper that wraps a call to New and panics if the error is non-nil. It is intended for use in variable initializations and startup code where template initialization failures should be fatal, such as:

var t = livetemplate.Must(livetemplate.New("app"))

This follows the same pattern as html/template.Must and text/template.Must.

func New

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

func (*Template) Clone

func (t *Template) Clone() (*Template, error)

Clone creates a deep copy of the template with fresh state. This is useful for creating per-connection template instances that don't interfere with each other.

func (*Template) Execute

func (t *Template) Execute(wr io.Writer, data interface{}, messages ...map[string]string) error

Execute applies a parsed template to the specified data object, writing the output to wr. It orchestrates all 5 phases:

Phase 1: Parse (already done via Parse/ParseFiles/ParseGlob)
Phase 2: Build - Generate tree structure
Phase 3: Diff - Compare with cached state (no-op for first render)
Phase 4: Render - Execute template to HTML
Phase 5: Send - Write HTML response

Note: Phases execute in order 1→4→5→2 (Render before Build) to minimize response latency. Tree building for caching happens after sending the response.

The optional messages parameter provides context for templates via the lvt namespace. It contains both field validation errors and flash messages (prefixed with "_flash:"). Field errors affect ResponseMetadata.Success; flash messages don't.

func (*Template) ExecuteUpdates

func (t *Template) ExecuteUpdates(wr io.Writer, data interface{}, messages ...map[string]string) error

ExecuteUpdates generates a tree structure of static and dynamic content that can be used by JavaScript clients to update changed parts efficiently. It orchestrates all 5 phases:

Phase 1: Parse (already done via Parse/ParseFiles/ParseGlob)
Phase 2: Build - Generate tree structure (includes Phase 3: Diff internally)
Phase 3: Diff - Compare with cached tree, return only changes (integrated in Build)
Phase 4: Render - Execute template (integrated in Build)
Phase 5: Send - Write JSON tree response

Caching behavior: - First call: Returns complete tree with static structure ("s" key) and dynamic values - Subsequent calls: Returns only dynamic values that have changed (cache-aware)

The optional messages parameter provides context for templates via the lvt namespace. It contains both field validation errors and flash messages (prefixed with "_flash:"). Field errors affect ResponseMetadata.Success; flash messages don't.

func (*Template) Funcs

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

Funcs registers a template.FuncMap that will be applied to all template parsing and execution.

func (*Template) Handle

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

Handle creates an http.Handler using the Controller+State pattern.

Controller: Singleton that holds dependencies (DB, Logger, etc.). Never cloned. State: Pure data that is cloned per session via serialization. Must be wrapped with AsState().

The Controller+State separation ensures dependencies are never accidentally shared across sessions while pure state data is cloned via serialization.

Example:

handler := tmpl.Handle(
    &TodoController{DB: db, Logger: logger},
    AsState(&TodoState{}),
)
http.Handle("/todos", handler)

Lifecycle methods (all optional):

  • Mount(state, ctx) - Called once when session is created
  • OnConnect(state, ctx) - Called on WebSocket connect/reconnect
  • OnDisconnect() - Called on WebSocket disconnect

Action methods have signature: func(state StateType, ctx *Context) (StateType, error)

func (*Template) Parse

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

Parse parses text as a template body for the template t. This matches the signature of html/template.Template.Parse().

func (*Template) ParseFiles

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

ParseFiles parses the named files and associates the resulting templates with t. This matches the signature of html/template.Template.ParseFiles().

func (*Template) ParseGlob

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

ParseGlob parses the template definitions from the files identified by the pattern. This matches the signature of html/template.Template.ParseGlob().

func (*Template) SetUploadRegistry added in v0.3.1

func (t *Template) SetUploadRegistry(registry interface{})

SetUploadRegistry sets the upload registry for this template instance. This should be called after cloning a template for a specific connection.

type TemplateSet added in v0.7.1

type TemplateSet struct {
	// FS is the embedded filesystem containing the template files.
	FS embed.FS

	// Pattern is the glob pattern for matching template files within FS.
	// Examples: "templates/*.tmpl", "*.tmpl"
	Pattern string

	// Namespace identifies the component type for this template set.
	// Used for documentation and debugging purposes.
	// Example: "dropdown" for templates like "lvt:dropdown:searchable:v1"
	Namespace string

	// Funcs provides additional template functions for this component.
	// These are merged with the base template functions when parsing.
	Funcs template.FuncMap
}

TemplateSet represents a collection of embedded templates from a component library. Components create TemplateSet instances to expose their templates for registration.

Example usage in a component library:

package dropdown

import "embed"

//go:embed templates/*.tmpl
var templateFS embed.FS

func Templates() *livetemplate.TemplateSet {
    return &livetemplate.TemplateSet{
        FS:        templateFS,
        Pattern:   "templates/*.tmpl",
        Namespace: "dropdown",
    }
}

Example usage in main.go:

tmpl, err := livetemplate.New("app",
    livetemplate.WithComponentTemplates(dropdown.Templates(), tabs.Templates()),
)

type UpdateResponse

type UpdateResponse = send.UpdateResponse

UpdateResponse wraps a tree update with metadata for form lifecycle. Tree is an opaque type representing the update payload - the client library handles this automatically. This is an alias for internal/send.UpdateResponse for backward compatibility.

type UploadAccessor added in v0.7.0

type UploadAccessor interface {
	HasUploads(name string) bool
	GetCompletedUploads(name string) []*uploadtypes.UploadEntry
}

UploadAccessor provides access to upload entries during action handling.

type UploadConfig added in v0.3.1

type UploadConfig = uploadtypes.UploadConfig

UploadConfig configures file upload behavior for a specific upload field. It mirrors Phoenix LiveView's allow_upload/3 configuration pattern.

Fields:

  • Accept: Allowed MIME types or extensions (e.g., []string{"image/*", ".pdf"})
  • MaxEntries: Maximum number of concurrent files (0 = unlimited)
  • MaxFileSize: Maximum file size in bytes (0 = unlimited)
  • AutoUpload: Whether to start upload automatically on file selection
  • ChunkSize: Chunk size for WebSocket uploads in bytes (default: 256KB)
  • External: Optional Presigner for direct-to-storage uploads (S3, GCS, etc.)

type UploadEntry added in v0.3.1

type UploadEntry = uploadtypes.UploadEntry

UploadEntry represents a single file upload with its state and metadata. Exposed to templates via .lvt.Uploads(name).

Fields:

  • ID: Unique identifier for this upload entry
  • ClientName: Original filename from the client
  • ClientType: MIME type reported by the client
  • ClientSize: File size in bytes
  • Progress: Upload progress percentage (0-100)
  • Valid: Whether the upload passed validation
  • Done: Whether the upload has completed
  • Error: Error message if validation or upload failed
  • TempPath: Server-side temporary file path (server uploads only)
  • BytesRecv: Bytes received so far (for progress tracking)
  • ExternalRef: Final storage reference (external uploads only, e.g., S3 URL)
  • CreatedAt: When the upload entry was created
  • CompletedAt: When the upload completed (zero if not done)

type UploadMeta added in v0.3.1

type UploadMeta = uploadtypes.UploadMeta

UploadMeta contains presigned upload configuration for external storage.

Fields:

  • Uploader: Client-side uploader identifier (e.g., "s3", "gcs", "azure")
  • URL: Presigned upload endpoint URL
  • Fields: Form fields for multipart/form-data POST requests (optional)
  • Headers: HTTP headers required for the upload request (e.g., Content-Type)

type WSCloseError added in v0.8.5

type WSCloseError struct {
	Code int
	Text string
}

WSCloseError represents a WebSocket close message.

func (*WSCloseError) Error added in v0.8.5

func (e *WSCloseError) Error() string

type WSConn added in v0.8.5

type WSConn interface {
	ReadMessage() (messageType int, p []byte, err error)
	WriteMessage(messageType int, data []byte) error
	Close() error
}

WSConn is the interface for a WebSocket connection. Implementations can wrap gorilla/websocket, gws, gobwas/ws, coder/websocket, or any other WebSocket library.

type WSUpgrader added in v0.8.5

type WSUpgrader interface {
	Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (WSConn, error)
}

WSUpgrader upgrades an HTTP connection to a WebSocket connection.

Directories

Path Synopsis
internal
build
Package build handles tree building from parsed templates and data.
Package build handles tree building from parsed templates and data.
context
Package context provides template execution context utilities for the LiveTemplate library.
Package context provides template execution context utilities for the LiveTemplate library.
diff
Package diff provides tree comparison and differential update generation for LiveTemplate.
Package diff provides tree comparison and differential update generation for LiveTemplate.
fuzz/app
Package app provides application-level state and operations for fuzz testing.
Package app provides application-level state and operations for fuzz testing.
fuzz/generators
Package generators provides random data generation for fuzz testing.
Package generators provides random data generation for fuzz testing.
fuzz/mutations
Package mutations provides state mutation types and operations for fuzz testing.
Package mutations provides state mutation types and operations for fuzz testing.
jsonutil
Package jsonutil provides shared json-iterator configuration used across internal packages.
Package jsonutil provides shared json-iterator configuration used across internal packages.
keys
Package keys provides key generation for LiveTemplate.
Package keys provides key generation for LiveTemplate.
observe
Package observe provides operational metrics for LiveTemplate.
Package observe provides operational metrics for LiveTemplate.
render
Package render provides HTML rendering utilities for LiveTemplate.
Package render provides HTML rendering utilities for LiveTemplate.
send
Package send provides message formatting and serialization for LiveTemplate.
Package send provides message formatting and serialization for LiveTemplate.
session
Package session provides connection and session management for LiveTemplate.
Package session provides connection and session management for LiveTemplate.
util
Package util provides generic utility functions that can be used across the livetemplate codebase.
Package util provides generic utility functions that can be used across the livetemplate codebase.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL