Lucerna.Gates (Lucerna v0.0.1-alpha.0)

Server-side gates reads. The hot path has no process: an ETS lookup, a pure evaluation, done. No call funnels through a mailbox.

Guarantees (mirroring the Ruby/Node server clients):

  • Reads never raise. Before the first snapshot (or against a bad identity, or a malformed rule) they answer safe fallbacks: flag false, experiment nil, switch true, evaluate empty. Stale beats default, default beats crash.
  • Fail-safe, then fail-stale. No snapshot yet → fallbacks with reason :uninitialized. Once a snapshot exists, sync failures serve the stale table — a crashed poller never degrades reads to "off".

Every point read emits [:lucerna, :gates, :evaluation] telemetry with %{name, type, key, reason} metadata.

All functions take name: MyLucerna in opts for non-default instances; the default instance name is Lucerna.

Summary

Functions

One identity's decisions over the entire runtime — for SSR or bootstrapping a client. Never records exposures. No runtime yet: empty maps.

The assigned variant name, or nil (not assigned / unknown key / no runtime yet).

The flag's decision for this identity: true / false. Unknown key or no runtime yet: false.

Debug projection of one key across all three gate kinds, with the rule-by-rule trail. Never records exposures.

The last-known compiled runtime document (wire shape), or nil.

Kill switch state — false means the guarded path IS killed. Unknown key or no runtime yet: true (not killed).

Blocks until the first runtime load succeeds (true), the timeout elapses (false), or the first settle was a 401/403 (raises Lucerna.AuthError — a bad key should be discoverable). Optional to call: reads are safe before ready. Network errors and 5xx keep waiting — polling retries them.

Functions

evaluate(identity \\ nil, opts \\ [])

@spec evaluate(
  term(),
  keyword()
) :: %{kills: map(), flags: map(), experiments: map()}

One identity's decisions over the entire runtime — for SSR or bootstrapping a client. Never records exposures. No runtime yet: empty maps.

experiment(key, identity \\ nil, opts \\ [])

@spec experiment(String.t(), term(), keyword()) :: String.t() | nil

The assigned variant name, or nil (not assigned / unknown key / no runtime yet).

This is the only read that records an exposure — and only when a variant was assigned to a known user (an async cast to Lucerna.Reporting; a dead Reporting process never breaks a read).

flag(key, identity \\ nil, opts \\ [])

@spec flag(String.t(), term(), keyword()) :: boolean()

The flag's decision for this identity: true / false. Unknown key or no runtime yet: false.

identity is nil (anonymous), a Lucerna.Identity, or a loose map/keyword with user_id / email / traits — lenient by design, reads never raise.

inspect_gate(key, identity \\ nil, opts \\ [])

@spec inspect_gate(String.t(), term(), keyword()) :: map()

Debug projection of one key across all three gate kinds, with the rule-by-rule trail. Never records exposures.

runtime(opts \\ [])

@spec runtime(keyword()) :: map() | nil

The last-known compiled runtime document (wire shape), or nil.

switch(key, opts \\ [])

@spec switch(
  String.t(),
  keyword()
) :: boolean()

Kill switch state — false means the guarded path IS killed. Unknown key or no runtime yet: true (not killed).

wait_until_ready(opts \\ [])

@spec wait_until_ready(keyword()) :: boolean()

Blocks until the first runtime load succeeds (true), the timeout elapses (false), or the first settle was a 401/403 (raises Lucerna.AuthError — a bad key should be discoverable). Optional to call: reads are safe before ready. Network errors and 5xx keep waiting — polling retries them.

Options: :timeout (ms, default :infinity), :name.