Skip to content
Zever
Home

Feature flags

Gate code paths with typed flags from a static file or Firebase Remote Config.

2 min readEdit on GitHub

flag is a typed on/off switch that is safe in a hot request handler. The interface exposes Bool, String, Int, and JSON lookups against per-request targeting context from WithEvalContext. A missing key returns your fallback with a nil error, so a flag lookup can never take the app down.

static (the default) loads a JSON file once and costs no network call per evaluation. Use it for tests, offline dev, and rollouts that can ship with a deploy.

Terminal window
FLAG_ADAPTER=static
FLAG_STATIC_PATH=data/flags.json
FLAG_STATIC_RELOAD=false
main.go
import (
"context"
"github.com/zenta-dev/zever/adapters/flag/static"
"github.com/zenta-dev/zever/config"
"github.com/zenta-dev/zever/container"
"github.com/zenta-dev/zever/core/flag"
)
static.Register() // container wires nothing
c := container.New(config.Default())
f, err := c.Flag()
if err != nil {
panic(err)
}
defer f.Close()
ctx := flag.WithEvalContext(context.Background(), flag.EvalContext{
RandomizationID: "user-123",
Signals: map[string]any{"plan": "pro"},
})
on, err := f.Bool(ctx, "new_checkout", false)
fmt.Println(on, err)

Expected output (empty static set, so the fallback applies):

false <nil>

An empty static path is valid and means an empty flag set. Because the fallback is false, a missing key or an outage keeps serving the old checkout.

Use firebase when product or growth must change a flag live. It adds a real dependency and a 30s default operation timeout (DefaultTimeout). Durations in files are integer nanoseconds.

Terminal window
FLAG_ADAPTER=firebase
FLAG_FIREBASE_PROJECT_ID=my-project
FLAG_FIREBASE_SERVICE_ACCOUNT=/run/secrets/firebase.json
FLAG_FIREBASE_TIMEOUT=30s
Terminal window
FLAG_ADAPTER=firebase
FLAG_FIREBASE_PROJECTID=my-project
FLAG_FIREBASE_TIMEOUT=30s

Validate rejects only a negative firebase.timeout; zero means the default. Adapters are static (adapters/flag/static, the default) and firebase (adapters/flag/firebase, Remote Config; needs project_id and service_account). See the flag config reference.

  • Flag.Close() error takes no context. The container probes it during Close(ctx), joins errors with errors.Join, and surfaces panics as *ClosePanicError.
  • static is per-process, so a file change only lands where the file lands (plus reload when enabled). firebase is the shared source of truth, so design fallbacks so every replica degrades the same way when it is unreachable.
  • Never log raw option maps (use cfg.RedactedServices()). The service account is a file path, not key material, but keep it out of logs.
A flag always returns the fallback

Check that the static file path resolves relative to the working directory (an empty or missing path means an empty set by design), and that the key spelling matches. A hierarchy such as billing.checkout_v2 avoids collisions.

Type mismatch error on an existing key

The stored JSON type does not match the accessor (String on a bool key returns the fallback plus an error). Align the file value with the call, or read it via JSON.

Firebase open fails or times out

Verify project_id, the service_account JSON path, and egress to the Remote Config endpoint. Only a negative timeout fails Validate, so a hang is a network problem, not a config shape problem.