Documentation
¶
Overview ¶
Package circuit provides an embeddable HTTP UI for viewing and editing application configuration structs with automatic file persistence.
What Circuit Does ¶
Circuit generates a web UI from a Go config struct and provides:
- Automatic form generation from struct tags
- File-backed persistence (YAML, JSON, TOML supported)
- Automatic file watching and in-memory reload
- Optional authentication (Basic Auth, Forward Auth)
- Optional executable actions (restart, reload, flush, etc.)
What Circuit Doesn't Do ¶
Circuit does not automatically restart your application or apply changes to running components. Your application is responsible for:
- Restarting services when config changes
- Applying new config values to active components
- Validating config changes before applying them
Use the WithOnChange callback to hook into config updates and trigger your application's reload logic.
Basic Usage ¶
The typical workflow is:
- Define a config struct with yaml and circuit tags
- Load config from disk (your responsibility)
- Create handler via circuit.From(&cfg, options...)
- Mount handler on your existing http.ServeMux
Example (minimal embed with net/http):
package main
import (
"log"
"net/http"
"github.com/moq77111113/circuit"
)
type Config struct {
Host string `yaml:"host" circuit:"type:text,help:Server hostname"`
Port int `yaml:"port" circuit:"type:number,help:Server port,required,min:1,max:65535"`
TLS bool `yaml:"tls" circuit:"type:checkbox,help:Enable TLS"`
}
func main() {
var cfg Config
// Create handler - Circuit will load initial config from file
handler, err := circuit.From(&cfg,
circuit.WithPath("config.yaml"),
circuit.WithTitle("My App Settings"),
)
if err != nil {
log.Fatal(err)
}
// Mount on existing mux
mux := http.NewServeMux()
mux.Handle("/config", handler)
log.Println("Circuit UI available at http://localhost:8080/config")
http.ListenAndServe(":8080", mux)
}
Struct Tags ¶
Circuit uses the circuit struct tag to configure form fields. Use circuit:"-" to hide sensitive fields like passwords or API keys.
Tag format: circuit:"type:INPUT_TYPE,ATTRIBUTE:VALUE,FLAG,..."
Supported input types:
- text, password, email, url, tel
- number, range
- checkbox
- select (requires options attribute)
- date, time, color
Common attributes:
- help:TEXT - help text shown below the field
- min:N, max:N, step:N - numeric constraints
- minlen:N, maxlen:N - string length constraints
- pattern:REGEX - regex validation pattern
- options:k1=v1;k2=v2 - select/radio options
Common flags:
- required - field must not be empty
- readonly - field cannot be edited
Example (struct tags):
type Config struct {
// Text input with length constraints
Name string `yaml:"name" circuit:"type:text,help:Service name,required,minlen:2,maxlen:50"`
// Number input with range constraints
Port int `yaml:"port" circuit:"type:number,help:Listen port,required,min:1,max:65535"`
// Select input with options
LogLevel string `yaml:"log_level" circuit:"type:select,options:debug=Debug;info=Info;warn=Warning;error=Error"`
// Hidden field - Circuit will ignore this
APIKey string `yaml:"api_key" circuit:"-"`
}
Security ¶
Circuit UIs should be protected. Editing config can be dangerous - protect the endpoint with authentication or place it behind a reverse proxy.
Use WithAuth() to enable authentication:
// Development: plaintext password (DO NOT use in production)
auth := circuit.NewBasicAuth("admin", "dev-password")
// Production: argon2id hash
auth := circuit.NewBasicAuth("admin", "$argon2id$v=19$m=65536,t=3,p=4$...")
handler, _ := circuit.From(&cfg,
circuit.WithPath("config.yaml"),
circuit.WithAuth(auth),
)
For reverse proxy setups (OAuth2 Proxy, Traefik ForwardAuth, Cloudflare Access):
auth := circuit.NewForwardAuth("X-Forwarded-User", map[string]string{
"email": "X-Forwarded-Email",
})
Actions ¶
Actions enable operators to trigger safe, application-defined operations like restarting workers or flushing caches.
Actions are registered via WithActions() and appear as buttons in the UI:
restart := circuit.NewAction("restart_worker", "Restart Worker", func(ctx context.Context) error {
return worker.Restart(ctx)
}).Describe("Safely restarts the background worker").Confirm().WithTimeout(10 * time.Second)
flush := circuit.NewAction("flush_cache", "Flush Cache", func(ctx context.Context) error {
cache.Clear()
return nil
}).Describe("Clears all cached data")
h, _ := circuit.From(&cfg, circuit.WithActions(restart, flush))
Safety notes for actions:
- Actions run server-side code - ensure they are safe by default
- Use .Confirm() for destructive operations (restarts, deletions)
- Always use timeouts - default is 30 seconds
- Avoid shelling out unless necessary (prefer native Go APIs)
File Watching and Hot Reload ¶
Circuit automatically watches the config file and reloads the in-memory struct when changes are detected. Use WithOnChange to be notified:
handler, _ := circuit.From(&cfg,
circuit.WithPath("config.yaml"),
circuit.WithOnChange(func(e circuit.ChangeEvent) {
log.Printf("config reloaded from %s", e.Source)
// Your responsibility: apply new config to running components
server.ApplyConfig(cfg)
}),
)
Change sources:
- SourceFormSubmit - user submitted the form
- SourceFileChange - file changed on disk
- SourceManual - handler.Apply() was called directly
Disable file watching with WithAutoWatch(false) if you want manual reload only.
Index ¶
- Constants
- type Action
- type Authenticator
- type BasicAuth
- type ChangeEvent
- type ForwardAuth
- type Handler
- type Identity
- type OnChange
- type Option
- func WithActions(actions ...Action) Option
- func WithAuth(a Authenticator) Option
- func WithAutoApply(enable bool) Option
- func WithAutoSave(enable bool) Option
- func WithAutoWatch(enable bool) Option
- func WithBrand(b bool) Option
- func WithOnChange(fn OnChange) Option
- func WithOnError(fn func(error)) Option
- func WithPath(path string) Option
- func WithReadOnly(enable bool) Option
- func WithSaveFunc(fn SaveFunc) Option
- func WithTitle(title string) Option
- type SaveFunc
- type Source
Examples ¶
Constants ¶
const ( // SourceFormSubmit indicates the change came from a user submitting the web form. SourceFormSubmit = events.SourceFormSubmit // SourceFileChange indicates the change came from the file watcher detecting // an external modification to the config file. SourceFileChange = events.SourceFileChange // SourceManual indicates the change came from calling handler.Apply() directly. SourceManual = events.SourceManual )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
Name string
Label string
Description string
Run func(context.Context) error
Timeout time.Duration
RequireConfirmation bool
}
Action defines an executable server-side operation displayed as a button in the Circuit UI.
Actions enable operators to trigger safe, application-defined operations like:
- Restarting workers or background jobs
- Flushing caches or clearing queues
- Running maintenance tasks
- Triggering manual sync operations
SAFETY REQUIREMENTS:
- Actions run server-side code - ensure they are safe by default
- Use .Confirm() for destructive operations (restarts, deletions, flushes)
- Always use timeouts to prevent hanging (default: 30s, can be customized)
- Avoid shelling out unless necessary - prefer native Go APIs
- Never use actions for privileged operations (system updates, user management)
- Validate inputs if actions accept parameters
Actions are registered via WithActions() and appear in the Actions section of the UI.
Example (creating actions with constructor):
// Safe action - no confirmation needed
flush := circuit.NewAction("flush_cache", "Flush Cache", func(ctx context.Context) error {
cache.Clear()
return nil
}).Describe("Clears all cached data")
// Destructive action - requires confirmation
restart := circuit.NewAction("restart_worker", "Restart Worker", func(ctx context.Context) error {
return worker.Restart(ctx)
}).Describe("Safely restarts the background worker").Confirm().WithTimeout(10 * time.Second)
h, _ := circuit.From(&cfg, circuit.WithActions(flush, restart))
func NewAction ¶
NewAction creates a new action with required fields.
Parameters:
- name: Unique identifier (used in URLs and logs)
- label: Display name shown on the button
- run: Function to execute (receives context for cancellation/timeout)
The run function MUST respect the provided context:
- Check ctx.Done() for long-running operations
- Return when context is cancelled
- Use context-aware APIs (http.NewRequestWithContext, db.QueryContext, etc.)
Default timeout is 30 seconds. Use .WithTimeout() for longer operations.
Optional configuration via fluent builder methods:
action := circuit.NewAction("restart", "Restart Worker", runFunc).
Describe("Safely restarts the worker").
Confirm().
WithTimeout(10 * time.Second)
func (Action) Confirm ¶
Confirm enables a confirmation dialog before execution.
Use for destructive or irreversible operations:
- Restarting services or workers
- Deleting data or clearing caches
- Flushing queues or buffers
- Triggering expensive operations
When enabled, the operator must explicitly click "Confirm" before the action runs. This helps prevent accidental execution of dangerous operations.
Example:
circuit.NewAction("restart", "Restart Service", restartFunc).Confirm()
func (Action) Describe ¶
Describe sets the action description shown in the UI. Helps operators understand what the action does before triggering it.
func (Action) WithTimeout ¶
WithTimeout sets custom execution timeout. Default is 30 seconds.
Use for long-running operations that need more time:
- Database migrations or large queries
- Cache warming operations
- Batch processing tasks
- External API calls with slow response times
The timeout is enforced via context cancellation. Your action's run function MUST respect the context and return promptly when ctx.Done() is signaled.
Example:
circuit.NewAction("migrate", "Run Migration", migrateFunc).WithTimeout(5 * time.Minute)
type Authenticator ¶
Authenticator validates HTTP requests and returns identity information.
Implementations must be safe for concurrent use.
The Authenticate method is called on EVERY request to the Circuit handler. Return an error to reject the request with 401 Unauthorized.
Built-in implementations:
- BasicAuth - HTTP Basic Authentication
- ForwardAuth - Reverse proxy header authentication
type BasicAuth ¶
BasicAuth implements HTTP Basic Authentication with support for plaintext and argon2id hashed passwords.
IMPORTANT: For production use, always use argon2id password hashes, not plaintext. Plaintext passwords should ONLY be used in development/testing environments.
Argon2id hashes are automatically detected by the "$argon2id$v=19$" prefix. Circuit supports the PHC string format output by golang.org/x/crypto/argon2.
Operational security recommendations:
- Store credentials in a separate file (e.g., /etc/myapp/auth.conf)
- Set file permissions to 0640 or stricter (readable only by app user)
- Never store credentials in your config struct (they would be editable via Circuit UI)
- Use environment variables or secret management systems in production
func NewBasicAuth ¶
NewBasicAuth creates an authenticator that validates via HTTP Basic Auth.
The password can be:
- Plaintext (for dev/testing ONLY) - never use in production
- Argon2id PHC hash (for production) - auto-detected by "$argon2id$v=19$" prefix
Circuit supports argon2id hashes in PHC string format as output by the golang.org/x/crypto/argon2 package. The hash format is:
$argon2id$v=19$m=MEMORY,t=TIME,p=PARALLELISM$SALT$HASH
Example with plaintext (DEVELOPMENT ONLY):
auth := circuit.NewBasicAuth("admin", "dev-password")
ui, _ := circuit.From(&cfg, circuit.WithAuth(auth))
Example with argon2id hash (PRODUCTION):
// Generate hash with: golang.org/x/crypto/argon2
// Example hash:
hash := "$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$..."
auth := circuit.NewBasicAuth("admin", hash)
ui, _ := circuit.From(&cfg, circuit.WithAuth(auth))
Operational security:
- Never store credentials in your app's config struct
- Store credentials in /etc/myapp/auth.conf with 0640 permissions
- Use a secret management system (Vault, AWS Secrets Manager, etc.)
- Rotate passwords regularly
type ChangeEvent ¶
type ChangeEvent = events.ChangeEvent
ChangeEvent describes a configuration change.
Delivered to OnChange callbacks after the in-memory config has been updated. Your application is responsible for applying the new config to running components.
type ForwardAuth ¶
ForwardAuth implements authentication via reverse proxy headers. Common with OAuth2 Proxy, Traefik ForwardAuth, Cloudflare Access.
func NewForwardAuth ¶
func NewForwardAuth(subjectHeader string, claimHeaders map[string]string) *ForwardAuth
NewForwardAuth creates an authenticator that validates via reverse proxy headers. Common with OAuth2 Proxy, Traefik ForwardAuth, Cloudflare Access.
The subjectHeader must contain the authenticated user identifier. Optional claimHeaders can map claim names to header names for metadata extraction.
Example:
auth := circuit.NewForwardAuth("X-Forwarded-User", map[string]string{
"email": "X-Forwarded-Email",
"role": "X-Auth-Role",
})
ui, _ := circuit.From(&cfg, WithAuth(auth))
Example ¶
ExampleNewForwardAuth demonstrates reverse proxy authentication. Common with OAuth2 Proxy, Traefik ForwardAuth, Cloudflare Access.
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"github.com/moq77111113/circuit"
)
func main() {
type Config struct {
Setting string `yaml:"setting" circuit:"type:text"`
}
tmpDir := os.TempDir()
configPath := filepath.Join(tmpDir, "example_forward.yaml")
_ = os.WriteFile(configPath, []byte("setting: value\n"), 0644)
defer func() {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
log.Printf("failed to remove %s: %v", configPath, err)
}
}()
var cfg Config
// Configure forward auth to read headers from reverse proxy
auth := circuit.NewForwardAuth("X-Forwarded-User", map[string]string{
"email": "X-Forwarded-Email",
"role": "X-Auth-Role",
})
handler, err := circuit.From(&cfg,
circuit.WithPath(configPath),
circuit.WithAuth(auth),
)
if err != nil {
log.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
// Request with proxy headers - should succeed
req, _ := http.NewRequest("GET", server.URL, nil)
req.Header.Set("X-Forwarded-User", "user@example.com")
req.Header.Set("X-Forwarded-Email", "user@example.com")
resp, _ := http.DefaultClient.Do(req)
fmt.Println("Status:", resp.StatusCode)
}
Output: Status: 200
func (*ForwardAuth) Authenticate ¶
Authenticate validates the request via proxy headers.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves the Circuit UI and provides manual control methods.
The handler implements http.Handler and can be mounted on any mux.
Manual control methods are available for advanced workflows:
- Apply(formData) - manually apply form changes (preview mode with WithAutoApply(false))
- Save() - manually persist to disk (manual save with WithAutoSave(false))
Example (preview mode):
h, _ := circuit.From(&cfg, circuit.WithAutoApply(false))
http.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && r.FormValue("confirm") == "yes" {
r.ParseForm()
if err := h.Apply(r.Form); err != nil {
http.Error(w, err.Error(), 500)
return
}
}
h.ServeHTTP(w, r)
})
func From ¶
From creates and returns a Handler that serves a small web UI for inspecting and editing a YAML-backed configuration value.
From validates that `cfg` is a pointer to a struct (used to extract schema information from struct tags), applies any provided Option values and then attempts to load the initial configuration from the path supplied via `WithPath`. If successful it starts a file watcher that reloads the configuration on changes and returns a handler wired to that loader.
The returned Handler implements http.Handler and exposes manual control methods:
- Apply(formData) - manually apply form data (for preview mode)
- Save() - manually save config to disk
Common errors:
- when `cfg` is not a pointer
- when no path is provided (use `WithPath`)
- when schema extraction, initial load, or watcher setup fails
Example (Actions) ¶
ExampleFrom_actions demonstrates executable actions. Actions appear as buttons in the UI and run server-side code.
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"time"
"github.com/moq77111113/circuit"
)
func main() {
type Config struct {
CacheSize int `yaml:"cache_size" circuit:"type:number"`
}
// Create temporary config file
tmpDir := os.TempDir()
configPath := filepath.Join(tmpDir, "example_actions.yaml")
_ = os.WriteFile(configPath, []byte("cache_size: 100\n"), 0644)
defer func() {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
log.Printf("failed to remove %s: %v", configPath, err)
}
}()
var cfg Config
// Define safe actions with timeouts
// NOTE: This is a dummy action for demonstration
flushCache := circuit.NewAction("flush_cache", "Flush Cache", func(ctx context.Context) error {
// In real code, this would clear your cache
time.Sleep(100 * time.Millisecond) // Simulate work
return nil
}).Describe("Clears all cached data")
// Destructive actions should require confirmation
restart := circuit.NewAction("restart_worker", "Restart Worker", func(ctx context.Context) error {
// In real code, this would restart your worker
time.Sleep(100 * time.Millisecond) // Simulate work
return nil
}).Describe("Safely restarts the background worker").Confirm().WithTimeout(10 * time.Second)
handler, err := circuit.From(&cfg,
circuit.WithPath(configPath),
circuit.WithActions(flushCache, restart),
)
if err != nil {
log.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
fmt.Println("Status:", resp.StatusCode)
}
Output: Status: 200
Example (Auth) ¶
ExampleFrom_auth demonstrates Basic Auth integration. Shows both plaintext (dev) and argon2id (production) authentication.
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"github.com/moq77111113/circuit"
)
func main() {
type Config struct {
Host string `yaml:"host" circuit:"type:text"`
Port int `yaml:"port" circuit:"type:number"`
}
// Create temporary config file
tmpDir := os.TempDir()
configPath := filepath.Join(tmpDir, "example_auth.yaml")
_ = os.WriteFile(configPath, []byte("host: localhost\nport: 8080\n"), 0644)
defer func() {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
log.Printf("failed to remove %s: %v", configPath, err)
}
}()
var cfg Config
// Development: plaintext password (DO NOT use in production)
// auth := circuit.NewBasicAuth("admin", "dev-password")
// Production: argon2id PHC hash
// Hash generated with: golang.org/x/crypto/argon2
// This example uses a plaintext password for testing purposes
auth := circuit.NewBasicAuth("admin", "secure-password")
handler, err := circuit.From(&cfg,
circuit.WithPath(configPath),
circuit.WithAuth(auth),
)
if err != nil {
log.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
// Request without auth - should fail
resp, _ := http.Get(server.URL)
fmt.Println("Without auth:", resp.StatusCode)
// Request with auth - should succeed
req, _ := http.NewRequest("GET", server.URL, nil)
req.SetBasicAuth("admin", "secure-password")
resp, _ = http.DefaultClient.Do(req)
fmt.Println("With auth:", resp.StatusCode)
}
Output: Without auth: 401 With auth: 200
Example (Minimal) ¶
ExampleFrom_minimal demonstrates the simplest Circuit integration. Creates a handler for a config struct and mounts it on a standard http.ServeMux.
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"github.com/moq77111113/circuit"
)
func main() {
// Define config struct with circuit tags
type Config struct {
Host string `yaml:"host" circuit:"type:text,help:Server hostname"`
Port int `yaml:"port" circuit:"type:number,help:Server port,required,min:1,max:65535"`
TLS bool `yaml:"tls" circuit:"type:checkbox,help:Enable TLS"`
}
// Create temporary config file
tmpDir := os.TempDir()
configPath := filepath.Join(tmpDir, "example_minimal.yaml")
_ = os.WriteFile(configPath, []byte("host: localhost\nport: 8080\ntls: false\n"), 0644)
defer func() {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
log.Printf("failed to remove %s: %v", configPath, err)
}
}()
var cfg Config
// Create Circuit handler
handler, err := circuit.From(&cfg,
circuit.WithPath(configPath),
circuit.WithTitle("Example Config"),
)
if err != nil {
log.Fatal(err)
}
// Mount on standard mux
mux := http.NewServeMux()
mux.Handle("/config", handler)
// Test with httptest
server := httptest.NewServer(mux)
defer server.Close()
resp, _ := http.Get(server.URL + "/config")
fmt.Println("Status:", resp.StatusCode)
}
Output: Status: 200
Example (OnChange) ¶
ExampleFrom_onChange demonstrates config change notifications. Circuit notifies your app when config changes, allowing you to apply updates.
package main
import (
"fmt"
"log"
"net/http/httptest"
"os"
"path/filepath"
"github.com/moq77111113/circuit"
)
func main() {
type Config struct {
Workers int `yaml:"workers" circuit:"type:number"`
}
tmpDir := os.TempDir()
configPath := filepath.Join(tmpDir, "example_onchange.yaml")
_ = os.WriteFile(configPath, []byte("workers: 4\n"), 0644)
defer func() {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
log.Printf("failed to remove %s: %v", configPath, err)
}
}()
var cfg Config
handler, err := circuit.From(&cfg,
circuit.WithPath(configPath),
circuit.WithOnChange(func(e circuit.ChangeEvent) {
fmt.Printf("Config changed from %s\n", e.Source)
// Your responsibility: apply new config to running components
// Example: workerPool.Resize(cfg.Workers)
}),
)
if err != nil {
log.Fatal(err)
}
server := httptest.NewServer(handler)
defer server.Close()
fmt.Println("Handler created")
}
Output: Handler created
func (*Handler) Apply ¶
Apply manually applies form data to the in-memory config.
Used in preview mode (WithAutoApply(false)) to confirm changes after user review. The typical workflow:
- User submits form (POST)
- Circuit renders preview with submitted values (doesn't modify memory yet)
- User reviews and confirms
- Your code calls Apply(formData) to commit changes
Respects WithAutoSave setting: if enabled, changes are saved to disk after apply. If WithAutoSave(false), call Save() separately to persist.
func (*Handler) Save ¶
Save manually persists the current in-memory config to disk.
Used in manual save mode (WithAutoSave(false)) to control when persistence happens. Common use cases:
- Batch multiple changes before saving
- Add validation or approval workflows before persisting
- Trigger saves on external events (timers, signals)
Uses custom SaveFunc if provided via WithSaveFunc, otherwise writes YAML to the path specified in WithPath.
type OnChange ¶
OnChange is called when configuration changes.
The callback is invoked AFTER the in-memory config struct has been updated. Use this to:
- Apply new config to running services
- Log config changes
- Trigger restart of affected components
- Validate config before applying
Example:
circuit.WithOnChange(func(e circuit.ChangeEvent) {
log.Printf("config updated from %s", e.Source)
server.ApplyConfig(cfg)
})
type Option ¶
type Option func(*config)
Option configures behavior passed to `From`.
func WithActions ¶
WithActions registers executable server-side actions in the UI.
Actions appear as buttons in the Actions section and allow operators to trigger safe, application-defined operations like restarting workers or flushing caches.
Each action is created with NewAction(name, label, run) and can be configured via:
- .Describe(text) - add help text
- .Confirm() - require user confirmation before execution
- .WithTimeout(duration) - set execution timeout (default: 30s)
Example:
restart := circuit.NewAction("restart", "Restart Worker", func(ctx context.Context) error {
return worker.Restart(ctx)
}).Describe("Safely restarts the worker").Confirm().WithTimeout(10 * time.Second)
circuit.WithActions(restart)
Safety notes:
- Actions run server-side code - ensure they are safe by default
- Use .Confirm() for destructive operations (restarts, deletions, flushes)
- Always use timeouts to prevent hanging operations
- Avoid shelling out unless necessary (prefer native Go APIs)
- Action failures are displayed in the UI
func WithAuth ¶
func WithAuth(a Authenticator) Option
WithAuth sets the authenticator for the Circuit UI.
Default: nil (no authentication - UI is publicly accessible).
IMPORTANT: Circuit UIs should always be protected. Editing config can be dangerous. Use authentication or place the handler behind a reverse proxy.
Built-in authenticators:
- NewBasicAuth(username, password) - HTTP Basic Auth
- NewForwardAuth(header, claims) - Reverse proxy headers
Example with Basic Auth:
auth := circuit.NewBasicAuth("admin", "$argon2id$v=19$...")
circuit.WithAuth(auth)
Example with Forward Auth (OAuth2 Proxy):
auth := circuit.NewForwardAuth("X-Forwarded-User", nil)
circuit.WithAuth(auth)
The authenticator is called on EVERY request to the handler.
func WithAutoApply ¶
WithAutoApply controls whether form submissions automatically update the in-memory config struct.
Default: true (auto-apply enabled).
When true: Form submission (POST) immediately updates the config struct in memory and saves to disk (if WithAutoSave(true)).
When false: Form submission renders a preview with the submitted values but does NOT modify the config in memory. You must call handler.Apply(formData) manually to confirm the changes.
Use preview mode (false) when you want to:
- Review changes before applying them
- Add custom validation before updates
- Implement approval workflows
Example (preview mode):
h, _ := circuit.From(&cfg, circuit.WithAutoApply(false))
http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && r.FormValue("confirm") == "yes" {
r.ParseForm()
h.Apply(r.Form) // Manually confirm
}
h.ServeHTTP(w, r)
})
func WithAutoSave ¶
WithAutoSave controls whether config changes are automatically persisted to disk.
Default: true (auto-save enabled).
When true: After updating the in-memory config, Circuit automatically writes the new config to the file specified in WithPath.
When false: The in-memory config is updated, but the file is NOT written. You must call handler.Save() manually to persist changes.
Use manual save (false) when you want to:
- Batch multiple changes before writing to disk
- Add custom validation before persistence
- Control when disk I/O happens
Example (manual save):
h, _ := circuit.From(&cfg, circuit.WithAutoSave(false))
// Later, after validating changes:
if err := h.Save(); err != nil {
log.Printf("failed to save config: %v", err)
}
func WithAutoWatch ¶
WithAutoWatch controls whether file watching and automatic reload are enabled.
Default: true (file watching enabled).
When true: Circuit watches the config file for changes and automatically reloads the in-memory struct when the file is modified.
When false: File watching is disabled. Config is only reloaded when:
- User submits the web form
- handler.Apply() is called manually
Disable auto-watch if you want manual control over when config is reloaded, or if you're running in an environment without inotify support.
func WithBrand ¶
WithBrand controls whether the Circuit footer/brand is shown in the UI.
Default: true (brand is shown).
Set to false to hide the "Powered by Circuit" footer:
circuit.WithBrand(false)
func WithOnChange ¶
WithOnChange registers a callback for configuration change events.
The callback is invoked AFTER the in-memory config struct has been updated. Your application is responsible for applying the new config to running components.
The ChangeEvent indicates the source of the change:
- SourceFormSubmit - user submitted the web form
- SourceFileChange - file changed on disk (file watcher)
- SourceManual - handler.Apply() was called directly
Example:
circuit.WithOnChange(func(e circuit.ChangeEvent) {
log.Printf("config updated from %s", e.Source)
server.ApplyConfig(cfg) // Your responsibility
})
func WithOnError ¶
WithOnError registers a callback for errors during file watching or reload.
Common error scenarios:
- File watcher errors (permissions, inotify limits)
- Config parse errors (invalid YAML/JSON/TOML)
- File read errors (deleted file, network mount issues)
The callback is invoked for non-fatal errors. Fatal errors (initial load failure) are returned by From() directly.
Example:
circuit.WithOnError(func(err error) {
log.Printf("config reload failed: %v", err)
})
func WithPath ¶
WithPath sets the filesystem path to the configuration file.
This option is REQUIRED - From will return an error if no path is provided.
Supported formats are auto-detected by extension:
- .yaml, .yml - YAML format
- .json - JSON format
- .toml - TOML format
Circuit will:
- Load the initial config from this file on startup
- Watch the file for changes (unless WithAutoWatch(false))
- Persist updates to this file (unless WithAutoSave(false))
If the file doesn't exist, From returns an error. Create the file first or use a custom SaveFunc to handle initialization.
func WithReadOnly ¶
WithReadOnly makes the UI read-only, preventing all edits.
Default: false (UI is editable).
When true:
- All input fields are disabled
- Save button is hidden
- Add/Remove slice item buttons are hidden
- Form submission is blocked
Use read-only mode when you want to:
- Expose config for viewing without allowing edits
- Provide a "status" page for operators
- Show config in production environments where edits must go through CI/CD
Example:
circuit.WithReadOnly(true)
func WithSaveFunc ¶
WithSaveFunc replaces the default file writing with custom persistence logic.
Default: Circuit writes to the file path using the detected format (YAML/JSON/TOML).
Use a custom SaveFunc when you need to:
- Store config in a database or remote storage
- Add encryption or compression before writing
- Validate config before persisting
- Notify external systems about config changes
The SaveFunc receives the current config value and path. It is called:
- After form submission (if WithAutoSave(true))
- When handler.Save() is called manually
Example (custom persistence):
circuit.WithSaveFunc(func(cfg any, path string) error {
// Validate before saving
if err := validateConfig(cfg); err != nil {
return err
}
// Write to database instead of file
return db.SaveConfig(cfg)
})