A real-time counter application demonstrating LiveTemplate's reactive state management and tree-based optimization.
Start the server:
From project root:
go run examples/counter/main.go
Or from the counter directory:
cd examples/counter
go run main.go
With custom port:
PORT=8081 go run main.go
With environment-based configuration:
# Development mode with connection limits
LVT_DEV_MODE=true LVT_MAX_CONNECTIONS=100 go run main.go
# Production mode with allowed origins
LVT_ALLOWED_ORIGINS="https://example.com" LVT_LOG_LEVEL=info go run main.go
Open your browser:
Navigate to http://localhost:8080
Interact with the counter:
This example uses LiveTemplate's environment-based configuration system. All configuration is loaded from environment variables with the LVT_ prefix:
| Variable | Default | Description |
|---|---|---|
LVT_DEV_MODE |
false |
Enable development mode (uses local client library) |
LVT_MAX_CONNECTIONS |
0 (unlimited) |
Maximum concurrent WebSocket connections |
LVT_MAX_CONNECTIONS_PER_GROUP |
0 (unlimited) |
Maximum connections per session group |
LVT_ALLOWED_ORIGINS |
empty | Comma-separated list of allowed WebSocket origins |
LVT_LOG_LEVEL |
info |
Logging level (debug, info, warn, error) |
LVT_METRICS_ENABLED |
true |
Enable Prometheus metrics export |
LVT_SHUTDOWN_TIMEOUT |
30s |
Graceful shutdown timeout |
Example configurations:
# Development
LVT_DEV_MODE=true LVT_LOG_LEVEL=debug go run main.go
# Production with limits
LVT_MAX_CONNECTIONS=10000 LVT_ALLOWED_ORIGINS="https://example.com" go run main.go
# Disable metrics for testing
LVT_METRICS_ENABLED=false go run main.go
For more details, see CONFIGURATION.md.
The server is extremely simple with the new reactive API:
// Controller: singleton, holds dependencies (none in this simple example)
type CounterController struct{}
// State: pure data, cloned per session
type CounterState struct {
Title string `json:"title" lvt:"persist"`
Counter int `json:"counter" lvt:"persist"`
LastUpdated string `json:"last_updated" lvt:"persist"`
}
// Named action methods — routed via <button name="increment">
func (c *CounterController) Increment(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
state.Counter++
state.LastUpdated = formatTime()
return state, nil
}
func (c *CounterController) Decrement(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
state.Counter--
state.LastUpdated = formatTime()
return state, nil
}
func (c *CounterController) Reset(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
state.Counter = 0
state.LastUpdated = formatTime()
return state, nil
}
func main() {
envConfig, _ := livetemplate.LoadEnvConfig()
controller := &CounterController{}
initialState := &CounterState{Title: "Live Counter", Counter: 0, LastUpdated: formatTime()}
tmpl := livetemplate.Must(livetemplate.New("counter", envConfig.ToOptions()...))
http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))
http.ListenAndServe(":8080", nil)
}
Key concepts:
<button name="increment"> routes to Increment() method.tmpl, .html, .gotmpl filesAsState()Zero-config integration - just add one script tag:
<!-- In your template — standard HTML, no special attributes needed -->
<button name="increment">+1</button>
<button name="decrement">-1</button>
<button name="reset">Reset</button>
<!-- Auto-initializing client library -->
<script src="livetemplate-client.js"></script>
That's it! No JavaScript code needed. The client library auto-initializes and handles:
<button name="increment"> routes to Increment() method/live endpointActions use standard HTML forms with button name routing and hidden inputs for data:
<!-- Simple action via button name -->
<button name="increment">+1</button>
<!-- Form with multiple fields -->
<form method="POST" name="add">
<input name="title" type="text">
<input name="priority" type="number">
<button type="submit">Add</button>
</form>
<!-- Data via hidden inputs -->
<form method="POST">
<input type="hidden" name="id" value="123">
<button name="delete">Delete Item</button>
</form>
Form field values are accessed in the controller via ctx.GetString(), ctx.GetInt(), or ctx.BindAndValidate():
func (c *Controller) Delete(state State, ctx *livetemplate.Context) (State, error) {
id := ctx.GetInt("id")
// ...
}
lvt-on:click - Route click events on non-button elements (e.g., table rows)lvt-on:keydown - Handle keyboard eventslvt-mod:debounce - Custom timing control for event routinglvt-fx:scroll - Auto-scroll behaviorlvt-fx:animate - Entry/exit animationslvt-form:preserve - Prevent form auto-resetlvt-form:no-intercept - Skip WebSocket, use real HTTP POSTBrowser WebSocket/HTTP Go Server
┌─────────────────┐ ┌──────────┐ ┌──────────────────┐
│ counter.tmpl │ │ │ │ CounterState │
│ (rendered HTML) │ │ │ │ implements │
│ │ │ │ │ Store │
│ [+1] [-1] [Reset]◄──────►│ /live │◄─────────────►│ │
│ │ │ │ │ Change(action, │
│ LiveTemplate │ │ │ │ data) │
│ Client JS │ │ │ │ │
│ │ │ │ │ Handle() │
│ Button name │ │ Auto- │ │ - Clones state │
│ routing │ │ detects │ │ - Generates │
│ (Tier 1 HTML) │ │ transport│ │ updates │
│ │ │ │ │ - Broadcasts │
└─────────────────┘ └──────────┘ └──────────────────┘
Initial State (counter = 0):
{
"s": ["<!DOCTYPE html><html>...", "...</html>"],
"0": "Live Counter",
"1": "0",
"2": "zero",
"3": "Counter is zero",
"4": "2025-09-30 00:20:00",
"5": "session-1727654400"
}
After Increment (only changed values):
{
"1": "1",
"2": "positive",
"3": "Counter is positive",
"4": "2025-09-30 00:20:05"
}
This demonstrates LiveTemplate's bandwidth efficiency - subsequent updates contain only the 4 changed dynamic values instead of the full HTML document.
The template follows the same pattern as testdata/e2e/counter/input.tmpl:
:8080, can be overridden with PORT environment variable/live handles both WebSocket upgrades and HTTP POST requestsexamples/counter/counter.tmplclient/dist/livetemplate-client.browser.js via internal/testing.ServeClientLibrary() (development only - use CDN in production)cd client && npm run build to regenerate the browser bundleThe counter uses the Controller+State pattern introduced in v0.7.0:
// Controller: singleton, holds dependencies
controller := &CounterController{}
// State: pure data, cloned per session
initialState := &CounterState{Title: "Live Counter", Counter: 0}
tmpl := livetemplate.Must(livetemplate.New("counter"))
http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))