Help us shape the future of Go! Take the 2026 Go Developer Survey

settings

package module
v0.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

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.

Settings declared with OrgOverridable also accept a value per organisation (ADR-0056). Setting.Get returns the organisation's value when the context's actor acts in one (actor.Actor.OrgID, set by orgs.RequireMember), and the platform value otherwise.

Stability: stable (ADR-0015, ADR-0054).

Index

Examples

Constants

View Source
const DefaultMaxItems = 100

DefaultMaxItems limits a StringList setting declared without MaxItems, so a list is never bounded only by the request body limit.

View Source
const DefaultResyncInterval = 5 * time.Minute

DefaultResyncInterval is how often a running Store reloads every value, covering notifications lost while disconnected.

Variables

View Source
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")

	// ErrNotOrgOverridable reports an organisation value for a setting not
	// declared with [OrgOverridable].
	ErrNotOrgOverridable = errors.New("settings: setting can't be changed per organisation")

	// ErrInvalidOrgID reports an empty or malformed organisation ID.
	ErrInvalidOrgID = errors.New("settings: invalid organisation ID")

	// 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.

View Source
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.

Functions

This section is empty.

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
	// OrgID is the organisation whose value changed; empty for a
	// platform-wide change.
	OrgID     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() string

Error names the setting key and the reason the value was rejected.

func (*InvalidValueError) Unwrap

func (e *InvalidValueError) Unwrap() error

Unwrap returns ErrInvalidValue.

type Kind

type Kind string

Kind is a setting's value type.

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) Option

Describe sets the help text shown to operators.

func Email

func Email() Option

Email 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) Option

Group sets the group used to organise settings in listings. Default: the key's first segment.

func MaxItems

func MaxItems(n int) Option

MaxItems limits a StringList setting to n items. Without it, a StringList setting is limited to DefaultMaxItems.

func MaxLen

func MaxLen(n int) Option

MaxLen limits a String setting, or each item of a StringList, to n characters.

func OneOf

func OneOf(values ...string) Option

OneOf limits a String setting, or each item of a StringList, to values.

func OrgOverridable

func OrgOverridable() Option

OrgOverridable lets each organisation have its own value, within the same validation, through Store.SetForOrg (ADR-0056). Setting.Get returns it when the context's actor acts in that organisation. Only mark settings an organisation may choose for itself: never security-relevant ones such as sign-in, rate limits, retention, maintenance or email senders. It can't be combined with RestartRequired.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"gorbital.dev/actor"
	"gorbital.dev/modules/settings"
)

func main() {
	reg := settings.NewRegistry()
	invitationTTL := settings.Duration(reg, "orgs.invitation_ttl", 7*24*time.Hour,
		settings.Range(24*time.Hour, 30*24*time.Hour),
		settings.OrgOverridable(),
	)

	// orgs.RequireMember returns a context whose actor acts in the
	// organisation. Get returns the organisation's own value, set with
	// Store.SetForOrg, when it has one, and the platform value otherwise.
	ctx := actor.With(context.Background(), actor.Actor{Kind: actor.KindUser, ID: "usr_1", OrgID: "org_1"})
	fmt.Println(invitationTTL.Get(ctx))
}
Output:
168h0m0s

func Range

func Range[T int | float64 | time.Duration](lo, hi T) Option

Range 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() Option

ReasonRequired makes every change to the setting require a reason, recorded in its history. Use it for security-relevant settings.

func RestartRequired

func RestartRequired() Option

RestartRequired makes Setting.Get return the value loaded at startup. Changes are stored immediately and take effect when instances restart.

func URL

func URL() Option

URL 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) Option

Validate 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() *Registry

NewRegistry returns an empty registry.

func (*Registry) Keys

func (r *Registry) Keys() []string

Keys returns the key of every declared setting, in declaration order. Setting keys are public API (ADR-0015); apps record them in their surface inventory (ADR-0054).

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.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"gorbital.dev/modules/settings"
)

func main() {
	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(),
	)

	// Until a store loads a changed value, Get returns the default.
	fmt.Println(codeTTL.Get(context.Background()))
}
Output:
15m0s

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. Combine with MaxItems; the default limit is DefaultMaxItems.

func (*Setting[T]) Get

func (s *Setting[T]) Get(ctx context.Context) T

Get 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. For a setting declared with OrgOverridable, when the actor in ctx acts in an organisation that has a valid value of its own, Get returns that value. Get never touches the database.

func (*Setting[T]) Key

func (s *Setting[T]) Key() string

Key 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.

Example
package main

import (
	"context"
	"log"

	"github.com/jackc/pgx/v5/pgxpool"

	"gorbital.dev/audit"
	"gorbital.dev/modules/settings"
)

func main() {
	ctx := context.Background()
	var (
		pool     *pgxpool.Pool  // from postgres.Open
		recorder audit.Recorder // for example an auditpg store
	)

	reg := settings.NewRegistry()
	maintenance := settings.Bool(reg, "ops.maintenance_mode", false,
		settings.Describe("Return 503 for all non-ops routes."),
		settings.ReasonRequired(),
	)

	store, err := settings.NewStore(ctx, pool, reg, recorder)
	if err != nil {
		log.Fatal(err)
	}
	// Run store alongside the HTTP server so changes from other instances
	// arrive: app.Run(ctx, []app.Runner{server, store}, ...).
	_ = store
	_ = maintenance
}

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) GetForOrg

func (s *Store) GetForOrg(orgID, key string) (View, error)

GetForOrg returns one setting as organisation orgID sees it, or ErrInvalidOrgID, ErrUnknownSetting or ErrNotOrgOverridable.

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) HistoryForOrg

func (s *Store) HistoryForOrg(ctx context.Context, orgID, key string, before int64, limit int) ([]HistoryEntry, error)

HistoryForOrg returns changes to organisation orgID's value of key, newest first, paged like Store.History.

func (*Store) List

func (s *Store) List() []View

List returns every declared setting in declaration order. It reads memory only.

func (*Store) ListForOrg

func (s *Store) ListForOrg(orgID string) ([]View, error)

ListForOrg returns the settings declared OrgOverridable, in declaration order, as organisation orgID sees them: Value is the organisation's own value when it has a valid one, else PlatformValue. It reads memory only. It returns ErrInvalidOrgID for an empty or malformed ID; checking that the organisation exists, and that the caller may see it, is the caller's job.

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) Overrides

func (s *Store) Overrides(ctx context.Context, key, after string, limit int) ([]View, error)

Overrides returns the organisations that have their own value of key, by organisation ID, as views for each organisation. For the next page, pass the last view's OrgID as after ("" starts from the first). limit is clamped to 1–100. A setting not declared OrgOverridable has none; an undeclared key returns ErrUnknownSetting.

func (*Store) Reload

func (s *Store) Reload(ctx context.Context) error

Reload reads every stored value from the database, organisation values included.

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) ResetForOrg

func (s *Store) ResetForOrg(ctx context.Context, orgID, key string, change Change) (View, error)

ResetForOrg removes organisation orgID's own value of key, so the organisation gets the platform value again. It returns the same errors as Store.SetForOrg, except for invalid values.

func (*Store) Run

func (s *Store) Run(ctx context.Context) error

Run 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) SetForOrg

func (s *Store) SetForOrg(ctx context.Context, orgID, key string, value json.RawMessage, change Change) (View, error)

SetForOrg validates value like Store.Set and stores it as organisation orgID's own value of key, a setting declared OrgOverridable. Pass the organisation view's Version. The change is recorded in the organisation's history and as a "settings.value.changed" audit event carrying the organisation, and applies on every instance within moments.

It returns the errors of Store.Set, ErrInvalidOrgID and ErrNotOrgOverridable. The organisation must exist; checking that the caller may change its settings is the caller's job.

func (*Store) UnknownKeys

func (s *Store) UnknownKeys() []string

UnknownKeys 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) StoreOption

WithLogger sets the logger for listener and invalid-value warnings. Default: discard.

func WithResyncInterval

func WithResyncInterval(d time.Duration) StoreOption

WithResyncInterval 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,
	// or, in a view for an organisation, the platform value.
	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. In a view
	// for an organisation, it is the version of the organisation's value.
	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

	// OrgOverridable reports a setting declared with [OrgOverridable].
	OrgOverridable bool
	// OrgID is the organisation of a view returned by [Store.ListForOrg],
	// [Store.GetForOrg], [Store.SetForOrg], [Store.ResetForOrg] or
	// [Store.Overrides]; empty for the platform-wide value.
	OrgID string
	// PlatformValue, in a view for an organisation, is the value the
	// organisation gets without its own: the effective platform-wide value.
	PlatformValue json.RawMessage
}

View is a setting's declaration and current state, for operator APIs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL