modules/settings
Package settings provides runtime settings: non-secret tunables declared in Go with a default and bounds, stored in PostgreSQL only when changed, and applied on every instance without a restart (ADR-0031).
Secrets and infrastructure (database URL, API keys, listen addresses) stay in the environment (ADR-0020); they can't be declared here.
reg := settings.NewRegistry()
codeTTL := settings.Duration(reg, "auth.verification_code_ttl", 15*time.Minute,
settings.Describe("How long email verification codes stay valid."),
settings.Range(5*time.Minute, time.Hour),
settings.ReasonRequired(),
)
store, err := settings.NewStore(ctx, pool, reg, recorder)
// Run store with the app's runners; pass codeTTL, a config.Value, to modules.
A Store loads every stored value at startup, applies changes made through Store.Set and Store.Reset immediately, and learns about changes made by other instances through PostgreSQL LISTEN/NOTIFY, with a periodic full reload as a fallback.
Stability: pre-1.0 (ADR-0015).
Constants#
const DefaultResyncInterval#
const DefaultResyncInterval = 5 * time.MinuteDefaultResyncInterval is how often a running Store reloads every value, covering notifications lost while disconnected.
Variables#
var ErrUnknownSetting, …#
var (
// ErrUnknownSetting reports a key that isn't declared in the registry.
ErrUnknownSetting = errors.New("settings: unknown setting")
// ErrVersionConflict reports that the setting changed after the caller
// read it. Read it again and retry.
ErrVersionConflict = errors.New("settings: setting changed since it was read")
// ErrReasonRequired reports a change without a reason to a setting
// declared with [ReasonRequired].
ErrReasonRequired = errors.New("settings: a reason is required to change this setting")
// ErrActorRequired reports a change without an authenticated actor in
// the context.
ErrActorRequired = errors.New("settings: changes require an authenticated actor")
// ErrInvalidValue reports a value that fails decoding or validation.
// The error is an [*InvalidValueError].
ErrInvalidValue = errors.New("settings: invalid value")
)Errors returned by Store methods. Check them with errors.Is.
var Migrations#
var Migrations fs.FS = mustSub(migrationFiles, "migrations")Migrations holds the module's goose migrations: the settings_values and settings_history tables. Apps copy them into db/migrations (the settings recipe does this); tests can apply them directly with pgtest.
Types#
type Change#
type Change struct {
// Version is the View.Version the caller last read.
Version int64
// Reason explains the change; required for ReasonRequired settings.
Reason string
}Change carries what a caller supplies with a change. The actor comes from the context.
type HistoryEntry#
type HistoryEntry struct {
ID int64
Key string
OldValue json.RawMessage
NewValue json.RawMessage
Version int64
Reason string
ActorKind actor.Kind
ActorID string
RequestID string
ChangedAt time.Time
}HistoryEntry is one change to a setting. A nil value means the default.
type InvalidValueError#
type InvalidValueError struct {
Key string
Reason string
}InvalidValueError describes why a value was rejected. Reason never contains the rejected value.
func (*InvalidValueError) Error#
func (e *InvalidValueError) Error() stringfunc (*InvalidValueError) Unwrap#
func (e *InvalidValueError) Unwrap() errorUnwrap returns ErrInvalidValue.
type Kind#
type Kind stringKind is a setting's value type.
const KindBool, …#
const (
KindBool Kind = "bool"
KindInt Kind = "int"
KindFloat Kind = "float"
KindString Kind = "string"
KindEnum Kind = "enum"
KindDuration Kind = "duration"
KindStringList Kind = "string_list"
)Setting kinds.
type Option#
type Option interface {
// contains filtered or unexported methods
}An Option configures a setting declaration. An option that doesn't fit the setting's kind makes the declaration panic at startup.
func Describe#
func Describe(text string) OptionDescribe sets the help text shown to operators.
func Email#
func Email() OptionEmail requires a String setting, or each item of a StringList, to be a bare email address such as no-reply@example.com.
func Group#
func Group(name string) OptionGroup sets the group used to organise settings in listings. Default: the key's first segment.
func MaxItems#
func MaxItems(n int) OptionMaxItems limits a StringList setting to n items.
func MaxLen#
func MaxLen(n int) OptionMaxLen limits a String setting, or each item of a StringList, to n characters.
func OneOf#
func OneOf(values ...string) OptionOneOf limits a String setting, or each item of a StringList, to values.
func Range#
func Range[T int | float64 | time.Duration](lo, hi T) OptionRange limits an Int, Float or Duration setting to [lo, hi]. The bounds' type must match the setting: Range(0.0, 2.0) for a Float.
func ReasonRequired#
func ReasonRequired() OptionReasonRequired makes every change to the setting require a reason, recorded in its history. Use it for security-relevant settings.
func RestartRequired#
func RestartRequired() OptionRestartRequired makes Setting.Get return the value loaded at startup. Changes are stored immediately and take effect when instances restart.
func URL#
func URL() OptionURL requires a String setting, or each item of a StringList, to be an absolute http or https URL.
func Validate#
func Validate[T any](fn func(T) error) OptionValidate adds a custom check. fn's parameter type must match the setting: func(time.Duration) error for a Duration. Error messages are shown to operators and must not include the value.
type Registry#
type Registry struct {
// contains filtered or unexported fields
}A Registry holds declared settings and their current values. Declare every setting before calling NewStore. A Registry is safe for concurrent use.
func NewRegistry#
func NewRegistry() *RegistryNewRegistry returns an empty registry.
type Setting#
type Setting[T any] struct {
// contains filtered or unexported fields
}A Setting is a typed handle to one declared setting. It implements config.Value, so library modules can accept it for live options.
func Bool#
func Bool(r *Registry, key string, def bool, opts ...Option) *Setting[bool]Bool declares a true/false setting.
func Duration#
func Duration(r *Registry, key string, def time.Duration, opts ...Option) *Setting[time.Duration]Duration declares a duration setting, stored and edited as a Go duration string such as "15m" or "1h30m". Combine with Range.
func Enum#
func Enum(r *Registry, key string, def string, allowed []string, opts ...Option) *Setting[string]Enum declares a text setting limited to allowed values.
func Float#
func Float(r *Registry, key string, def float64, opts ...Option) *Setting[float64]Float declares a decimal setting. Combine with Range, passing float bounds such as Range(0.0, 2.0).
func Int#
func Int(r *Registry, key string, def int, opts ...Option) *Setting[int]Int declares a whole-number setting. Combine with Range.
func String#
func String(r *Registry, key string, def string, opts ...Option) *Setting[string]String declares a text setting. Combine with MaxLen, URL or Email.
func StringList#
func StringList(r *Registry, key string, def []string, opts ...Option) *Setting[[]string]StringList declares a list-of-text setting, such as allowed origins. Get returns a copy the caller may modify.
func (*Setting[T]) Get#
func (s *Setting[T]) Get(context.Context) TGet returns the setting's current value. It returns the default while the setting is unchanged, when its stored value fails validation, and before a store has loaded. Settings declared with RestartRequired keep the value loaded at startup. Get never touches the database.
func (*Setting[T]) Key#
func (s *Setting[T]) Key() stringKey returns the setting's key.
type Store#
type Store struct {
// contains filtered or unexported fields
}Store persists settings in PostgreSQL and keeps a Registry current. It is an app.Runner: run it so changes from other instances arrive. It is safe for concurrent use.
func NewStore#
func NewStore(ctx context.Context, pool *pgxpool.Pool, reg *Registry, recorder audit.Recorder, opts ...StoreOption) (*Store, error)NewStore loads every stored value into reg and returns the store. After NewStore, reg accepts no new declarations. Changes are recorded as "settings.value.changed" audit events through recorder.
The settings tables come from Migrations; apply them first.
func (*Store) DeleteHistoryBefore#
func (s *Store) DeleteHistoryBefore(ctx context.Context, before time.Time, limit int) (int64, error)DeleteHistoryBefore deletes up to limit setting changes made before before, oldest first, and returns how many it deleted. Retention calls it until it deletes fewer than limit (ADR-0051).
func (*Store) Get#
func (s *Store) Get(key string) (View, error)Get returns one setting, or ErrUnknownSetting.
func (*Store) History#
func (s *Store) History(ctx context.Context, key string, before int64, limit int) ([]HistoryEntry, error)History returns changes to key, newest first. For the next page, pass the last entry's ID as before (0 starts from the newest). limit is clamped to 1–100.
func (*Store) List#
func (s *Store) List() []ViewList returns every declared setting in declaration order. It reads memory only.
func (*Store) OldestHistory#
func (s *Store) OldestHistory(ctx context.Context) (oldest time.Time, ok bool, err error)OldestHistory returns when the oldest recorded change was made; ok is false when there are none.
func (*Store) Reload#
func (s *Store) Reload(ctx context.Context) errorReload reads every stored value from the database.
func (*Store) Reset#
func (s *Store) Reset(ctx context.Context, key string, change Change) (View, error)Reset returns key to its default. It returns the same errors as Store.Set, except for invalid values.
func (*Store) Run#
func (s *Store) Run(ctx context.Context) errorRun keeps the registry current until ctx is done, then returns nil. It listens on a dedicated connection for changes committed by any instance, reloads everything after connecting (changes made while disconnected send no notification), and reloads everything every resync interval as a fallback. Connection failures are logged and retried with backoff.
func (*Store) Set#
func (s *Store) Set(ctx context.Context, key string, value json.RawMessage, change Change) (View, error)Set validates value (JSON, such as `"30m"` or `42`) and stores it for key. The change applies to this instance immediately and to others within moments. It returns ErrUnknownSetting, an *InvalidValueError, ErrReasonRequired, ErrActorRequired or ErrVersionConflict. Setting the current value again changes nothing.
func (*Store) UnknownKeys#
func (s *Store) UnknownKeys() []stringUnknownKeys returns stored keys that no declaration matches, for example settings removed from the code. Their rows are kept and ignored.
type StoreOption#
type StoreOption interface {
// contains filtered or unexported methods
}A StoreOption configures NewStore.
func WithLogger#
func WithLogger(logger *slog.Logger) StoreOptionWithLogger sets the logger for listener and invalid-value warnings. Default: discard.
func WithResyncInterval#
func WithResyncInterval(d time.Duration) StoreOptionWithResyncInterval sets how often Store.Run reloads every value. Default: DefaultResyncInterval.
type View#
type View struct {
Key string
Kind Kind
Group string
Description string
// Value is the effective value as JSON; Default is the declared default.
Value json.RawMessage
Default json.RawMessage
// Modified reports whether a valid stored value overrides the default.
Modified bool
// InvalidStoredValue reports a stored value that fails validation, so
// the default is in effect.
InvalidStoredValue bool
// Version increases with every change; pass it back to change the
// setting. It is 0 for a setting that has never been changed.
Version int64
UpdatedAt time.Time
UpdatedBy string
ReasonRequired bool
RestartRequired bool
// RestartPending reports a restart-required setting changed since this
// instance started.
RestartPending bool
// Constraints summarises validation, such as min, max, one_of, max_len,
// max_items and format. Treat it as read-only.
Constraints map[string]any
}View is a setting's declaration and current state, for operator APIs.