Clarify provider vs integration terminology and document the in-tree distribution model. Refresh M9 status, issue tracking, and lint/doc links to use the new language.
4.7 KiB
Server Package
This package implements the HTTP server for the member-console application, including HTMX partial handlers.
Overview
The server uses Go's standard net/http package with:
- Bootstrap 5 for styling
- HTMX 2.0 for dynamic content updates
- gorilla/csrf for CSRF protection
- scs (
alexedwards/scs) for server-side session management (Valkey/Redis-backed)
Architecture
The HTTP server handles several types of requests:
- Static Files (
/static/*) - CSS, JS, images served from embedded assets - Page Handlers (
/) - Full HTML page rendering - Partial Handlers (
/partials/*) - HTMX partial HTML responses - Auth Handlers (
/login,/logout,/callback) - OAuth2/OIDC authentication
Partial handlers interact with Temporal workflows for async operations like site provisioning.
Files
server.go- Main server setup, middleware chain, route registrationmember_products.go,member_invoices.go,workspace_partials.go,operator*.go- HTMX partial handlers for this package's own features
Integration-owned handlers (e.g. FedWiki's member-facing API and partials)
live in their own tree under internal/integrations/<slug>/web instead of
here — see docs/building-an-integration.md and
internal/integrations/fedwiki/web for the worked example. The patterns
below apply equally to handlers in either location.
HTMX Integration
Partial Handler Pattern
Partial handlers return HTML fragments (not full pages) that HTMX swaps into the DOM:
// Handler returns HTML fragment
func (h *Handler) GetItems(w http.ResponseWriter, r *http.Request) {
// ... fetch data ...
h.Templates.Render(w, "items.html", data)
}
<!-- Template returns partial HTML -->
<div class="table-responsive">
<table class="table">
{{ range .Sites }}
<tr>...</tr>
{{ end }}
</table>
</div>
HTMX Triggers
Use HX-Trigger header to trigger events that refresh other parts of the page:
// After successful operation, trigger refresh
w.Header().Set("HX-Trigger", `{"refreshSites": true}`)
<!-- Element listens for the trigger -->
<div hx-get="/partials/items"
hx-trigger="load, refreshItems from:body">
</div>
CSRF with HTMX
CSRF tokens are included via hx-headers on forms:
<form hx-post="/partials/items"
hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>
Or globally in the page template using hx-vals or a meta tag approach.
Synchronous Workflow Execution
Unlike fire-and-forget patterns, this server waits for Temporal workflows to complete:
func (h *Handler) CreateSite(w http.ResponseWriter, r *http.Request) {
// 1. Start the workflow
we, err := h.TemporalClient.ExecuteWorkflow(ctx, options, workflow, input)
// 2. Wait for completion (BLOCKING)
var result WorkflowOutput
err = we.Get(ctx, &result)
// 3. Render result based on workflow outcome
if !result.Success {
h.renderError(w, result.ErrorMessage)
return
}
h.renderSuccess(w, result)
}
Benefits:
- User sees accurate success/failure immediately
- Spinner shows during actual operation
- Error messages come from workflow
Trade-offs:
- Request blocks until workflow completes
- Long operations may timeout (mitigated by workflow retry handling)
Route Organization
Routes are organized by feature using handler structs:
// ItemPartialsHandler groups related routes
type ItemPartialsHandler struct { ... }
func (h *ItemPartialsHandler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /partials/items", h.GetItems)
mux.HandleFunc("POST /partials/items", h.CreateItem)
mux.HandleFunc("DELETE /partials/items/{id}", h.DeleteItem)
// ...
}
Error Rendering
Errors are rendered back into the same partial/modal that initiated the request:
func (h *Handler) renderCreateFormError(w http.ResponseWriter, userID int64, errMsg string) {
// Re-fetch form data
data := h.getCreateFormData(userID)
data.Error = errMsg
// Re-render the form partial with error displayed
h.Templates.Render(w, "create_form.html", data)
}
This keeps the user in context - they see the error in the same modal/form they were using.
Middleware Stack
handler = middleware.Logging(logger)(handler) // Request logging
handler = middleware.Recover(logger)(handler) // Panic recovery
handler = middleware.Compress()(handler) // Gzip compression
handler = middleware.Decompress()(handler) // Gzip decompression
handler = middleware.SecurityHeaders()(handler) // Security headers
handler = middleware.RequestID()(handler) // Request ID generation
handler = csrf.Protect(key, opts...)(handler) // CSRF protection