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

flags

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package flags provides feature flags: typed values declared in code with a default, overridden by FLAG_<NAME> configuration and by values stored in the database (globally or for one scope, such as a user or a tenant).

var NewCheckout = flags.Bool("new_checkout", false).Describe("Serve the new checkout")
var AdminTwoFactor = flags.Bool("admin_two_factor", true).Lock()

app, err := vukem.New(vukem.WithFlags(NewCheckout, AdminTwoFactor))
ff, err := app.Flags()
if ff.Enabled(ctx, NewCheckout) { ... }

A value is resolved, lowest precedence first, from the code default, the FLAG_<NAME> variable, the stored global value and the stored value for the scope in the context (WithScope). Locked flags always have their code default. Reads fail closed: when the store or the cache errors, the default (the code default, or FLAG_<NAME> when set) is returned and a warning is logged; a read never fails a request. A kill switch should therefore default to its safe value in code, or be locked.

See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#feature-flags.

Example
package main

import (
	"context"
	"fmt"

	"github.com/AlexGalhardo/vukem/config"
	"github.com/AlexGalhardo/vukem/flags"
)

func main() {
	newCheckout := flags.Bool("new_checkout", false).Describe("Serve the new checkout")
	adminMFA := flags.Bool("admin_two_factor", true).Lock()
	uploadLimit := flags.Int("upload_limit_mb", 10)

	// In an app: vukem.WithFlags(newCheckout, adminMFA, uploadLimit) and
	// app.Flags(), which adds the database store and the app cache.
	ff, err := flags.New(flags.Config{
		Flags: []flags.AnyFlag{newCheckout, adminMFA, uploadLimit},
		Env:   config.MapSource(map[string]string{"FLAG_UPLOAD_LIMIT_MB": "25"}),
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	ctx := context.Background()
	fmt.Println(ff.Enabled(ctx, newCheckout), ff.Enabled(ctx, adminMFA), flags.Get(ctx, ff, uploadLimit))
	fmt.Println(ff.Expose(ctx, newCheckout))
}
Output:
false true 25
map[new_checkout:false]

Index

Examples

Constants

View Source
const DefaultTTL = 30 * time.Second

DefaultTTL is how long stored values stay cached when Config.TTL is zero. Writes invalidate the cached values, so a change shows at once on every instance sharing the cache; instances with their own memory cache see it within the TTL.

View Source
const EnvPrefix = "FLAG_"

EnvPrefix prefixes the configuration key that overrides a flag's code default: FLAG_ plus the upper-cased name (FLAG_NEW_CHECKOUT).

View Source
const Table = "feature_flags"

Table is the table holding stored flag values.

Variables

View Source
var (
	ErrUnknown = errors.New("flags: unknown flag")
	ErrLocked  = errors.New("flags: flag is locked")
	ErrInvalid = errors.New("flags: invalid value")
	ErrNoStore = errors.New("flags: no store configured")
)

Errors returned by Set, Put and Reset.

Functions

func Get

func Get[T Type](ctx context.Context, f *Flags, flag Flag[T]) T

Get returns the value of flag for the scope in ctx. It never fails: on a store or cache error, or an invalid scope, it returns the default and logs a warning; for a flag that was not registered it returns the code default and logs an error (once per name).

func Migration

func Migration(id string) schema.Migration

Migration creates the flag table; register it with a timestamped id (vukem install:flags does):

flags.Migration("20261017120000_create_feature_flags_table")

func WithScope

func WithScope(ctx context.Context, scope Scope) context.Context

WithScope returns a context whose flag reads also apply the values stored for scope; stored scope values win over global ones. Set it once per request in middleware, after authentication, from an internal id ("user:" + u.AuthID()), never from raw input. A read with an invalid scope returns the default and logs a warning.

Types

type AnyFlag

type AnyFlag interface {
	// contains filtered or unexported methods
}

AnyFlag is a Flag of any type, for registration and allowlists. Only Flag implements it.

type Change

type Change struct {
	Flag      string
	Scope     Scope
	Old, New  string
	Sensitive bool
}

Change describes a write: the flag's resolved value in its scope before and after it. Sensitive is true for string flags, whose values may be anything: log or audit them only redacted (the admin panel records their length and a SHA-256 prefix).

func Put

func Put[T Type](ctx context.Context, f *Flags, scope Scope, flag Flag[T], value T) (Change, error)

Put stores a typed value of flag in scope; see Set.

type Config

type Config struct {
	// Flags are the registered flags; reading any other flag returns its
	// code default and logs an error.
	Flags []AnyFlag
	// Store keeps the stored values. Nil uses code defaults and
	// FLAG_<NAME> only, and writes fail with ErrNoStore.
	Store Store
	// Cache caches the stored values for TTL. Nil reads the store every
	// time. Use a cache shared by every instance (database or Redis) when
	// a flag is a kill switch that must apply at once.
	Cache *cache.Cache
	// TTL is how long stored values stay cached (DefaultTTL when zero).
	TTL time.Duration
	// Env resolves FLAG_<NAME> overrides (app.Source). When it also lists
	// its keys (config.Source does, for .env files and maps), a FLAG_ key
	// that names no flag is an error; process variables cannot be listed,
	// so a typo there goes unnoticed.
	Env config.Lookuper
	// Log receives the warnings of failed reads (slog.Default when nil).
	Log *slog.Logger
}

Config configures New.

type DatabaseStore

type DatabaseStore struct {
	// contains filtered or unexported fields
}

DatabaseStore keeps stored values in Table (SQLite, Postgres or MySQL).

func NewDatabaseStore

func NewDatabaseStore(db *database.DB, now func() time.Time) *DatabaseStore

NewDatabaseStore returns a store over db; now stamps updated_at (time.Now when nil).

func (*DatabaseStore) Delete

func (s *DatabaseStore) Delete(ctx context.Context, scope Scope, name string) error

Delete implements Store.

func (*DatabaseStore) Load

func (s *DatabaseStore) Load(ctx context.Context, scope Scope) (map[string]string, error)

Load implements Store.

func (*DatabaseStore) Save

func (s *DatabaseStore) Save(ctx context.Context, scope Scope, name, value string) error

Save implements Store.

type Flag

type Flag[T Type] struct {
	// contains filtered or unexported fields
}

Flag is a typed feature flag. Declare it once (a package-level value is fine: it is immutable) and register it with vukem.WithFlags.

func Bool

func Bool(name string, def bool) Flag[bool]

Bool declares a flag that is on or off.

func Int

func Int(name string, def int) Flag[int]

Int declares a flag holding an integer (a limit, a percentage).

func String

func String(name, def string) Flag[string]

String declares a flag holding a short string (a variant name).

func (Flag[T]) Default

func (f Flag[T]) Default() T

Default returns the code default.

func (Flag[T]) Describe

func (f Flag[T]) Describe(text string) Flag[T]

Describe returns a copy of f with a description shown in the admin panel.

func (Flag[T]) Description

func (f Flag[T]) Description() string

Description returns the description.

func (Flag[T]) Kind

func (f Flag[T]) Kind() Kind

Kind returns the kind of the flag's value.

func (Flag[T]) Lock

func (f Flag[T]) Lock() Flag[T]

Lock returns a copy of f that always has its code default: FLAG_<NAME> is a boot error, stored values are ignored and writes fail with ErrLocked. Lock security-critical flags so nobody can switch them off from the admin panel or the environment.

func (Flag[T]) Locked

func (f Flag[T]) Locked() bool

Locked reports whether the flag is locked.

func (Flag[T]) Name

func (f Flag[T]) Name() string

Name returns the flag name.

type Flags

type Flags struct {
	// contains filtered or unexported fields
}

Flags resolves and changes feature flags. It is safe for concurrent use.

func New

func New(cfg Config) (*Flags, error)

New validates cfg: flag names (lowercase letters, digits and underscores, starting with a letter), duplicates and FLAG_<NAME> values. Misconfiguration is returned here, at boot.

func (*Flags) All

func (f *Flags) All() []Info

All describes the registered flags in registration order.

func (*Flags) Enabled

func (f *Flags) Enabled(ctx context.Context, flag Flag[bool]) bool

Enabled reports whether a boolean flag is on for the scope in ctx.

func (*Flags) Expose

func (f *Flags) Expose(ctx context.Context, allow ...AnyFlag) map[string]any

Expose returns the values of the allowlisted flags for the scope in ctx, keyed by name, for views and Inertia props. Only the flags passed are included: never expose every flag to the browser. Flags that were not registered are left out (and logged).

func (*Flags) Require

func (f *Flags) Require(flag Flag[bool]) func(http.Handler) http.Handler

Require serves next only while flag is on for the scope in the request context; otherwise it responds 404, as if the route did not exist. A flag that was not registered is logged when Require is called (at boot) and always responds 404.

func (*Flags) Reset

func (f *Flags) Reset(ctx context.Context, scope Scope, name string) (Change, error)

Reset removes the stored value of the flag called name in scope, so it falls back to the global value (for a scope) or the default.

func (*Flags) Set

func (f *Flags) Set(ctx context.Context, scope Scope, name, value string) (Change, error)

Set stores the value of the flag called name in scope (Global for everyone). value is parsed by the flag's kind ("true", "42", "blue"). It fails with ErrUnknown, ErrLocked, ErrInvalid (also for an invalid scope) or ErrNoStore, or with the store's error. Readers sharing the cache see the new value at once.

func (*Flags) Share

func (f *Flags) Share(allow ...AnyFlag) func(http.Handler) http.Handler

Share adds the allowlisted flags to every Inertia page as the "flags" prop (see Expose); put it after inertia's middleware. Flags that were not registered are logged when Share is called (at boot) and left out.

func (*Flags) States

func (f *Flags) States(ctx context.Context, scope Scope) ([]State, error)

States returns every registered flag resolved for scope, read from the store without the cache. Unlike reads, it returns the store's error.

type Info

type Info struct {
	Name        string
	Description string
	Kind        Kind
	// Default is the encoded code default ("true", "10", "classic").
	Default string
	Locked  bool
}

Info describes a registered flag.

type Kind

type Kind string

Kind names the type of a flag's value.

const (
	KindBool   Kind = "bool"
	KindInt    Kind = "int"
	KindString Kind = "string"
)

Flag kinds.

type Scope

type Scope string

Scope names who a stored value applies to: an internal identifier such as "user:42" or "tenant:7", never raw user input. It is 1 to 191 bytes of lowercase letters, digits and ":._-"; Global ("") applies to everyone.

const Global Scope = ""

Global is the scope of values that apply to everyone.

type State

type State struct {
	Info

	// Value is the resolved value in the scope.
	Value string
	// Stored reports whether the scope has a stored value (ignored for
	// locked flags).
	Stored bool
}

State is a flag as the admin panel shows it. Its Default is the code default, or FLAG_<NAME> when set.

type Store

type Store interface {
	// Load returns the stored values of scope by flag name.
	Load(ctx context.Context, scope Scope) (map[string]string, error)
	// Save stores the value of name in scope.
	Save(ctx context.Context, scope Scope, name, value string) error
	// Delete removes the value of name in scope; a missing row is no error.
	Delete(ctx context.Context, scope Scope, name string) error
}

Store keeps stored values by scope (Global is "").

type Type

type Type interface{ bool | int | string }

Type is the set of types a flag can hold.

Jump to

Keyboard shortcuts

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