Lucerna SDK for Elixir

Feature gates, experiments and identity for Elixir and Phoenix apps — the lucerna hex package. The server-side member of the Lucerna SDK family (the shared evaluator ships as @lucerna/gates-core; the JS and Ruby server SDKs are the siblings), expressed as an OTP application: one child in your supervision tree, then read anywhere with no process on the hot path.

Install

# mix.exs
def deps do
[{:lucerna, "~> 0.0.1-alpha.0"}]
end

Requires Elixir ~> 1.18 (the SDK uses the standard-library JSON module). Two runtime dependencies, by design: finch for HTTP, telemetry for instrumentation.

You need a server key (ck_srv_… or ck_key_…) from your environment's settings. Browser keys (ck_client_…) are rejected at construction — they can't download gate rules.

Quickstart

Add one child to your supervision tree:

children = [
{Lucerna, server_key: System.fetch_env!("LUCERNA_SERVER_KEY")}
]

Then read anywhere — build the identity once per request, pass it in:

identity = Lucerna.Identity.new!(user_id: "u_42", email: "sofia@acme.com")
Lucerna.Gates.flag("settings_sso", identity) #=> true / false
Lucerna.Gates.experiment("checkout_copy", identity) #=> "urgent" / nil
Lucerna.Gates.switch("checkout") #=> false means killed
Lucerna.identify(identity) # self-sync to People — email/name travel here only

Using Phoenix? See the Phoenix guide for the identity-plug, edge-evaluate-then-assign, and LiveView patterns.

API

All reads live under Lucerna.Gates and take an optional identity (a Lucerna.Identity, a loose map/keyword, or nil for anonymous) plus opts (name: for non-default instances).

FunctionReturnsOne line
Lucerna.Gates.flag(key, identity \\ nil, opts \\ [])booleanIs the flag on for this identity? Unknown keys and a runtime that has not loaded yet both return false.
Lucerna.Gates.experiment(key, identity \\ nil, opts \\ [])String.t() or nilReturns the assigned variant name, or nil when unassigned. This is the only read that records an exposure.
Lucerna.Gates.switch(key, opts \\ [])booleanReturns false when the guarded path is killed. Unknown keys and a runtime that has not loaded yet both return true.
Lucerna.Gates.evaluate(identity \\ nil, opts \\ [])mapReturns one identity's decisions over the whole runtime for SSR or client bootstrap. Does not record exposures.
Lucerna.Gates.inspect_gate(key, identity \\ nil, opts \\ [])mapReturns a debug projection across all three gate kinds with the rule-by-rule trace. Does not record exposures.
Lucerna.Gates.runtime(opts \\ [])map or nilReturns the last-known compiled runtime document, or nil before the first successful download.
Lucerna.Gates.wait_until_ready(opts \\ [])booleanBlocks until the first download lands. Optional in production — reads are safe before it returns.
Lucerna.identify(identity, opts \\ []):okSelf-syncs an identity to People. This is the only path that transmits email or name.
Lucerna.flush(opts \\ []):okDelivers queued exposures and identifies immediately. Useful in scripts and tests.
Lucerna.Identity.new!(fields)Lucerna.Identity.t()Builds a validated identity. user_id is required and must be non-empty.

See Lucerna.Config for every start option.

Guarantees & semantics

Errors & failure modes

Configuration

Options go to the child spec — there is no application-env config by design. Times are milliseconds (BEAM convention).

OptionDefaultMeaning
:server_key(required)Server API key (ck_srv_… or ck_key_…). Browser keys (ck_client_…) are rejected at construction.
:base_urlhttps://api.uselucerna.appAPI host. Override this for local development against a different worker.
:refresh_interval10_000Poll cadence in milliseconds. Values below 1_000 are floored to 1_000.
:request_timeout5_000Per-request timeout in milliseconds for runtime, events, and identify calls.
:track_exposurestrueWhen true, variant reads on experiment/3 record exposures for delivery.
:identity_key:server_keyAPI key used by Lucerna.identify/1. Defaults to the same value as :server_key.
:nameLucernaInstance name. Everything derives from it — run isolated instances by giving each a name and passing name: to reads.

Development

mix test # includes the golden-vector parity suite
mix format --check-formatted
mix docs # build HexDocs locally
LUCERNA_E2E=1 mix test test/e2e_dogfood_test.exs # live dogfood (needs the API worker)