A real-time todo application demonstrating LiveTemplate's controller pattern with SQLite persistence, basic authentication, search, sorting, and pagination. Styled with Pico CSS.
cd todos
go run .
Open http://localhost:8080 and log in with alice / password.
With a custom port:
PORT=8081 go run .
The app uses LiveTemplate's controller pattern where each action maps to a typed method:
type TodoController struct {
Queries *db.Queries
}
func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
var input AddInput
if err := ctx.BindAndValidate(&input, validate); err != nil {
return state, err
}
// Create todo in database, reload list
return c.loadTodos(dbCtx, state, ctx.UserID())
}
func (c *TodoController) Toggle(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Delete(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) ClearCompleted(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Search(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Sort(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) NextPage(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) PrevPage(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
Actions are routed from HTML via form name and button name attributes (Tier 1 pattern):
<!-- Form name="add" routes to Add() method -->
<form method="POST" name="add">
<input type="text" name="text" placeholder="What needs to be done?" required />
<button type="submit" name="add">Add</button>
</form>
<!-- Hidden input passes data; form name routes to Toggle() -->
<form method="POST" name="toggle">
<input type="hidden" name="id" value="{{ .ID }}" />
<input type="checkbox" onchange="this.form.requestSubmit()" />
</form>
<!-- Button name routes to ClearCompleted() -->
<button name="clearCompleted">Clear Completed</button>
Basic auth with hardcoded demo users. ctx.UserID() returns the authenticated username, used to isolate each user's todos in SQLite:
auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
users := map[string]string{"alice": "password", "bob": "password"}
pass, ok := users[username]
return ok && pass == password, nil
})
SQLite via sqlc-generated queries. The db/ directory contains generated code from queries.sql. Schema migrations run automatically on startup, including detection and recreation of outdated schemas.
go test -v -run TestTodosE2E
Requires Docker for Chrome headless testing.
:8080, override with PORT environment variabletodos.db in the current directory (:memory: when TEST_MODE=1)e2etest.ServeClientLibrary in dev mode, CDN in production