# Lucerna in Phoenix

The supervision child spec *is* the Phoenix integration — there is no `lucerna_phoenix` package and no plug you must install. Add one child to your application tree, then read gates in plugs, controllers, and LiveViews.

## 1. Start the instance

Add Lucerna to `application.ex`, alongside your Repo and Endpoint:

```elixir
# lib/my_app/application.ex
def start(_type, _args) do
  children = [
    MyApp.Repo,
    {Lucerna, server_key: System.fetch_env!("LUCERNA_SERVER_KEY")},
    MyAppWeb.Endpoint
  ]

  Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
```

The instance downloads the compiled runtime at boot and polls every `refresh_interval` (default 10s). Reads are safe before the first download completes — they answer safe fallbacks (see the README's guarantees table). If a screen must not render until the runtime is loaded, call `Lucerna.Gates.wait_until_ready/1` in a release task; a web request should never block on it.

## 2. Build the identity once, in a plug

Resolve the current user into a `Lucerna.Identity` once per request and stash it on the conn. Every downstream read shares it — never rebuild it per call.

```elixir
# lib/my_app_web/plugs/lucerna_identity.ex
defmodule MyAppWeb.Plugs.LucernaIdentity do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    case conn.assigns[:current_user] do
      nil ->
        assign(conn, :identity, nil)

      user ->
        assign(conn, :identity, Lucerna.Identity.new!(user_id: user.public_id, email: user.email))
    end
  end
end
```

`user_id` must be a stable pseudonymous id (`user.public_id`), never the email. `email`/`name` are carried on the identity but are **only** sent to People by `Lucerna.identify/1` — gates reads transmit nothing.

Wire it into the browser pipeline after authentication:

```elixir
pipeline :browser do
  # ...
  plug MyAppWeb.Plugs.AuthenticateUser
  plug MyAppWeb.Plugs.LucernaIdentity
end
```

## 3. Evaluate at the edge, assign, then render

Read gates in the controller and pass plain booleans/strings into the template. Do not call `Lucerna.Gates.*` from inside HEEx — templates render markup, they don't make decisions.

```elixir
def index(conn, _params) do
  identity = conn.assigns.identity

  conn
  |> assign(:sso_enabled, Lucerna.Gates.flag("settings_sso", identity))
  |> assign(:checkout_live, Lucerna.Gates.switch("checkout"))
  |> render(:index)
end
```

```heex
<.link :if={@sso_enabled} navigate={~p"/settings/sso"}>Single sign-on</.link>
<.checkout :if={@checkout_live} />
```

## 4. LiveView: the freeze caveat

A flag read in `mount/3` is evaluated **once** and then frozen for the life of that LiveView process. That's correct for feature flags — a user shouldn't see the UI reshuffle mid-session — so evaluate in `mount` and assign:

```elixir
def mount(_params, session, socket) do
  identity = Lucerna.Identity.new!(user_id: session["user_id"])
  {:ok, assign(socket, :new_editor, Lucerna.Gates.flag("new_editor", identity))}
end
```

But a **kill switch** is meant to bite immediately — that's the whole point of one. A value frozen in `mount` won't. For guarded paths that must react to a kill within the poll window, re-evaluate on a timer and push the new value into the socket:

```elixir
def mount(_params, session, socket) do
  identity = Lucerna.Identity.new!(user_id: session["user_id"])
  if connected?(socket), do: :timer.send_interval(15_000, :recheck_kills)
  {:ok, assign(socket, :checkout_live, Lucerna.Gates.switch("checkout"))}
end

def handle_info(:recheck_kills, socket) do
  {:noreply, assign(socket, :checkout_live, Lucerna.Gates.switch("checkout"))}
end
```

Pick an interval at or above the runtime `refresh_interval` (10s) — a tighter timer just re-reads the same ETS snapshot. Pushing snapshot flips over `Phoenix.PubSub` so LiveViews re-evaluate the instant the runtime changes is a planned enhancement; the timer is the shipped pattern.

## 5. Identify users

Sync a user to People when you learn who they are (login, signup, trait change). This is the only call that transmits `email`/`name`:

```elixir
def handle_user_signed_in(user) do
  Lucerna.identify(
    Lucerna.Identity.new!(user_id: user.public_id, email: user.email, name: user.name)
  )
end
```

Delivery is async and fire-and-forget; the server upserts idempotently, so you never need to await it in a request. Repeat sends of an unchanged payload are skipped.

## 6. Observe with telemetry

Lucerna emits `:telemetry` events instead of logging. Attach them to your existing telemetry pipeline:

- `[:lucerna, :gates, :evaluation]` — every point read.
- `[:lucerna, :gates, :snapshot]` — a new runtime landed (`%{flags, experiments, kills}` counts).
- `[:lucerna, :sync, :success | :error]` — each poll (`%{duration}`).
- `[:lucerna, :reporting, :flush | :error]` — exposure/identify delivery.

See the README for the full metadata shape of each event.
