featureflag

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package featureflag is a minimal feature-flag primitive for GoFastr apps.

The package name avoids collision with the standard library's `flag`; users who want a shorter local name can alias on import:

import flag "github.com/DonaldMurillo/gofastr/core/featureflag"

The model is intentionally small: a flag has a global on/off switch, an optional percentage rollout, and explicit user/tenant allow lists. Rules are evaluated against an EvalContext that you build from the request (user, tenant, env, attributes).

Anything more elaborate — variants with weights, dependent flags, scheduled ramps — should be a separate evaluator. This package's only job is to make "is feature X on for this caller right now" trivial to express in handler code.

Wiring:

store := featureflag.NewMemoryStore()
store.Set(featureflag.Flag{Key: "new-checkout", Enabled: true, Rollout: 25})
featureflag.SetDefault(featureflag.NewEvaluator(store))

// later, in a handler
if featureflag.Bool(ctx, "new-checkout") {
    return newCheckout(w, r)
}
return oldCheckout(w, r)

Concurrency: the bundled memory store is safe for concurrent reads and writes. Evaluator and the package-level helpers are safe for concurrent use as long as the underlying store is.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bool

func Bool(ctx context.Context, key string) bool

Bool is a convenience wrapper around Default().Bool.

func SetDefault

func SetDefault(e *Evaluator)

SetDefault installs the process-wide evaluator used by the package- level helpers (Bool). Pass nil to disable and have Bool return false for every call.

func WithContext

func WithContext(ctx context.Context, ec EvalContext) context.Context

WithContext attaches an EvalContext to ctx. Middleware that knows the user and tenant should call this once per request; handlers then just call flag.Bool(ctx, ...).

Types

type EvalContext

type EvalContext struct {
	UserID   string
	TenantID string
	Env      string
	Attrs    map[string]string
}

EvalContext is the per-call subject identity used to evaluate a flag. Build one from the request — typically the authenticated user id and the resolved tenant id.

The Env field is matched by string equality against any rule that references it; this is how "dev only" or "staging only" flags work without coupling the flag definition to deployment plumbing.

Attrs is open-ended additional context (region, plan tier, etc.). The default evaluator doesn't consult Attrs, but a custom Store / Evaluator wrapper can. Future versions of this package may grow attribute-rule matching; storing them now keeps the migration cheap.

func FromContext

func FromContext(ctx context.Context) EvalContext

FromContext returns the attached EvalContext, or a zero value if none.

type Evaluator

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

Evaluator is the entry point for runtime checks.

func Default

func Default() *Evaluator

Default returns the installed evaluator, or nil if none.

func NewEvaluator

func NewEvaluator(s Store) *Evaluator

NewEvaluator wraps a Store. Passing nil yields an evaluator that answers every question with the caller's default — useful for tests that don't want to wire a store but still call featureflag.Bool.

func NewEvaluatorWithSalt

func NewEvaluatorWithSalt(s Store, salt string) *Evaluator

NewEvaluatorWithSalt wraps a Store with a process-private salt mixed into the rollout bucket hash. Set this when the rollout cohort needs to be unpredictable to attackers (kill switches whose evasion would re-enable a payments flow, etc.) — without a salt, FNV-1a buckets are deterministic from public flag keys.

The salt is process-private by convention; rotating it shuffles every cohort so use only when that's acceptable.

func (*Evaluator) Bool

func (e *Evaluator) Bool(ctx context.Context, key string) bool

Bool returns whether the named flag is on for the supplied context. Missing keys, storage errors, and disabled flags all return false.

The context's user / tenant lists are checked first, then the rollout percentage. The decision is stable across processes — same key + same subject id always hashes the same way.

func (*Evaluator) BoolDefault

func (e *Evaluator) BoolDefault(ctx context.Context, key string, fallback bool) bool

BoolDefault returns the supplied fallback when the named flag is genuinely absent from the store; existing flags evaluate normally (so an explicitly-disabled flag returns false, ignoring fallback).

Use this for kill switches where a typo or accidental delete must not silently re-enable the protected path. Example:

if app.Flags().BoolDefault(ctx, "kill-payments", true) {
    return payment.Block(...) // safe default: block on missing flag
}

type Flag

type Flag struct {
	Key     string
	Enabled bool
	Rollout int
	Users   []string
	Tenants []string
	Envs    []string
}

Flag is the rule definition stored for a key.

Enabled is the global kill switch; when false, Bool always returns false regardless of the other fields.

Rollout is a percentage (0–100) of subjects that see the flag as on, computed from a stable hash of the flag key and subject id. Set 0 for "off-by-default" and 100 to enable everywhere the kill switch allows.

Users and Tenants are explicit allow lists. A subject matching either list short-circuits to true (regardless of rollout), so flags can be force-enabled for testers and beta tenants.

Envs restricts the flag to specific deployment environments. When non-empty, the EvalContext's Env must match one of the listed values or the flag evaluates to false — even for explicitly allow-listed users. When empty, no environment restriction applies. The match is case-sensitive string equality.

type MemoryStore

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

MemoryStore is the bundled in-memory MutableStore implementation. Suitable for single-instance apps, tests, and as a cache layer in front of a persistent store.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore creates an empty MemoryStore.

func (*MemoryStore) All

func (m *MemoryStore) All() []Flag

All returns a snapshot of every defined flag, suitable for /admin listings. The result is a copy — mutations don't affect the store.

func (*MemoryStore) Delete

func (m *MemoryStore) Delete(key string) error

Delete removes a flag. Missing keys are not an error.

func (*MemoryStore) Get

func (m *MemoryStore) Get(_ context.Context, key string) (*Flag, error)

Get returns the stored flag, or (nil, nil) when absent.

func (*MemoryStore) Set

func (m *MemoryStore) Set(f Flag) error

Set creates or updates a flag definition. Empty Key returns an error.

type MutableStore

type MutableStore interface {
	Store
	Set(f Flag) error
	Delete(key string) error
}

MutableStore extends Store with write operations. The memory store implements it; persistent stores (db, redis) typically should too so admin tools can edit flags at runtime.

type SQLOption

type SQLOption func(*SQLStore)

SQLOption configures the SQL store.

func WithSQLDialect

func WithSQLDialect(dialect string) SQLOption

WithSQLDialect pins the dialect ("postgres" or "sqlite") instead of running the SELECT version() probe. Use this when the probe is known to fail (e.g. embedded sqlite with restricted features) or when you want construction to avoid any DB read at all.

func WithSQLTable

func WithSQLTable(name string) SQLOption

WithSQLTable overrides the default "feature_flags" table name.

type SQLStore

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

SQLStore is a SQL-backed MutableStore. Flags are persisted as individual rows; allow lists (Users, Tenants, Envs) live in JSON columns so the schema stays a single table.

Dialect is either set explicitly via WithSQLDialect or detected at construction via SELECT version() (matches Postgres; everything else is treated as sqlite-compatible).

Schema:

feature_flags(
    key      TEXT PRIMARY KEY,
    enabled  INTEGER NOT NULL,
    rollout  INTEGER NOT NULL,
    users    TEXT NOT NULL,    -- JSON array
    tenants  TEXT NOT NULL,    -- JSON array
    envs     TEXT NOT NULL     -- JSON array
)

func NewSQLStore

func NewSQLStore(db *sql.DB, opts ...SQLOption) (*SQLStore, error)

NewSQLStore constructs a SQLStore and ensures the table exists.

func (*SQLStore) All

func (s *SQLStore) All(ctx context.Context) ([]Flag, error)

All returns every defined flag — used by admin tooling.

func (*SQLStore) Delete

func (s *SQLStore) Delete(key string) error

Delete implements MutableStore.

func (*SQLStore) Get

func (s *SQLStore) Get(ctx context.Context, key string) (*Flag, error)

Get implements Store.

func (*SQLStore) Set

func (s *SQLStore) Set(f Flag) error

Set implements MutableStore. Upserts the flag definition.

type Store

type Store interface {
	Get(ctx context.Context, key string) (*Flag, error)
}

Store is the pluggable backend that holds flag definitions. Implementations must be safe for concurrent use.

Get returns (nil, nil) — not an error — when the key isn't defined. Evaluator treats that as "fall through to the supplied default."

Jump to

Keyboard shortcuts

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