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 ¶
- func Bool(ctx context.Context, key string) bool
- func ParseUserEntry(entry string) (tenantID, userID string, err error)
- func SetDefault(e *Evaluator)
- func UserEntry(tenantID, userID string) string
- func WithContext(ctx context.Context, ec EvalContext) context.Context
- func WithEvaluator(ctx context.Context, e *Evaluator) context.Context
- type EvalContext
- type Evaluator
- type Flag
- type MemoryStore
- type MutableStore
- type SQLOption
- type SQLStore
- type Store
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ParseUserEntry ¶ added in v0.96.0
ParseUserEntry reads a Flag.Users entry back into the tenant and user it names, the inverse of UserEntry. The entry splits at its first ':'; the part before it is the percent-escaped tenant id, and an entry with no ':' is a bare user id with no tenant. An empty user id, or a tenant part that is not valid percent-escaping, is an error.
func SetDefault ¶
func SetDefault(e *Evaluator)
SetDefault installs the process-wide evaluator the package-level helpers (Bool) use on a context that carries none (see WithEvaluator). Pass nil to disable and have Bool return false for every such call.
func UserEntry ¶ added in v0.96.0
UserEntry spells a Flag.Users entry for one user of one tenant: "tenant:user", with ':' and '%' in the tenant id percent-escaped ("a:b" is written "a%3Ab"). The user id is written as is, ':' included, since the entry splits at its first ':'. With no tenant it is the bare user id, or ":user" when the id holds a ':' (a bare "google:42" would name tenant google's user 42).
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, ...).
func WithEvaluator ¶ added in v0.96.0
WithEvaluator attaches e to ctx ahead of the process default, so Bool on ctx reads e. An App attaches its own evaluator to every request, which keeps two Apps in one process from reading each other's flags through Default. A nil e still counts: Bool on ctx then reports false instead of falling back to Default.
Types ¶
type EvalContext ¶
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. A TenantID left empty falls back to the tenant the framework resolved for the request (handler.GetTenantID), so a multi-tenant app's user entries stay scoped to their tenant.
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 EvaluatorFrom ¶ added in v0.96.0
EvaluatorFrom returns the evaluator WithEvaluator attached to ctx, or Default() when ctx carries none.
func NewEvaluator ¶
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 ¶
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 ¶
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 ¶
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.
A Users entry names one tenant's user: "acme:42" matches user 42 of tenant acme only, and a bare "42" matches user 42 only when the context has no tenant (a single-tenant app). The entry splits at its first ':', with ':' and '%' in the tenant id percent-escaped; build entries with UserEntry. Tenants holds tenant ids.
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, including the Users/Tenants/Envs slices: mutations (e.g. scrubbing PII before a render) don't affect the store.
SECURITY: the snapshot includes each flag's full definition; the Users and Tenants allow-lists carry raw subject ids / emails. That is PII, and the list of who is inside a gated cohort is itself a signal. Gate this behind admin auth before rendering it; do not hand the result to an anonymous introspection endpoint. Compare router.Routes(), which carries the same warning.
func (*MemoryStore) Delete ¶
func (m *MemoryStore) Delete(key string) error
Delete removes a flag. Missing keys are not an error.
func (*MemoryStore) Get ¶
Get returns the stored flag, or (nil, nil) when absent. The returned Flag owns its slices: mutating them does not affect the store.
func (*MemoryStore) Set ¶
func (m *MemoryStore) Set(f Flag) error
Set creates or updates a flag definition. Empty Key returns an error. The store keeps its own copy of the allow-list slices: mutating the caller's Flag (or its slices) after Set does not change what evaluate() consults.
type MutableStore ¶
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 ¶
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 ¶
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 ¶
NewSQLStore constructs a SQLStore and ensures the table exists.
func (*SQLStore) All ¶
All returns every defined flag, used by admin tooling.
SECURITY: as with MemoryStore.All, the rows carry each flag's raw Users / Tenants allow-lists. Gate this behind admin auth; cohort membership is PII and a signal in its own right.