# Feature flags

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

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

## Quick start

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

```sh
FLAG_ADAPTER=static
FLAG_STATIC_PATH=data/flags.json
FLAG_STATIC_RELOAD=false
```

```go title="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):

```text
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.

## Flip flags without a deploy

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.

```sh
FLAG_ADAPTER=firebase
FLAG_FIREBASE_PROJECT_ID=my-project
FLAG_FIREBASE_SERVICE_ACCOUNT=/run/secrets/firebase.json
FLAG_FIREBASE_TIMEOUT=30s
```

```sh
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](https://zenta-dev.github.io/zever/reference/config/#flag).

## Production checklist

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

## Troubleshooting

<details>
<summary>A flag always returns the fallback</summary>

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

<details>
<summary>Type mismatch error on an existing key</summary>

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

<details>
<summary>Firebase open fails or times out</summary>

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

## Related

- [Tenants](https://zenta-dev.github.io/zever/guides/operate/tenants/): Per-tenant targeting signals.
- [Observability](https://zenta-dev.github.io/zever/guides/operate/observability/): A span per flag-gated branch.
- [Rate limiting](https://zenta-dev.github.io/zever/guides/security/ratelimit/): Guard flagged endpoints.
