Lucerna. Identity
(Lucerna v0.0.1-alpha.0)
The shared "who is the user" primitive — one immutable value, built
once per request, that feeds both products: gates reads take it
positionally, Lucerna.identify/1 sends it to People. Mirrors
GatesIdentity / identity.current() in the JS SDKs and
Lucerna::Identity in the Ruby gem.
PII rules: user_id is a pseudonymous app id ("u_42"), never an
email. email and name are first-class fields — never traits — and
are only ever transmitted by identify, which routes them to People's
encrypted columns. Traits are plaintext targeting data.
Summary
Functions
Projection for gates evaluation: user id + traits, with the first-class email merged in as a trait fallback so the engine's email→domain derivation makes domain overrides work. Purely local — gates evaluation transmits nothing; only identify sends PII.
Builds a validated identity. user_id must be a non-empty string;
traits are stringified strictly (see Lucerna.Traits.normalize!/1).
Bad input is a programming error and raises ArgumentError.
Wire payload for POST /sdk/v1/people/identify. Note the rename:
local user_id -> wire appUserId. email/name keys are omitted
entirely when absent; traits are always present.
Coerces the accepted identity forms — %Lucerna.Identity{}, map,
keyword list, nil — into an identity (or nil for anonymous).
Strict: bad input is a programming error. Gates reads have their own
lenient path and never raise.
Types
@type t() :: %Lucerna.Identity{ email: String.t() | nil, name: String.t() | nil, traits: Lucerna.Traits.traits(), user_id: String.t() }
Functions
@spec for_gates(t()) :: %{user_id: String.t(), traits: Lucerna.Traits.traits()}
Projection for gates evaluation: user id + traits, with the first-class email merged in as a trait fallback so the engine's email→domain derivation makes domain overrides work. Purely local — gates evaluation transmits nothing; only identify sends PII.
Builds a validated identity. user_id must be a non-empty string;
traits are stringified strictly (see Lucerna.Traits.normalize!/1).
Bad input is a programming error and raises ArgumentError.
Examples
iex> identity = Lucerna.Identity.new!(user_id: "u_42", email: "sofia@acme.com", traits: %{plan: "pro"})
iex> {identity.user_id, identity.email, identity.traits}
{"u_42", "sofia@acme.com", %{"plan" => "pro"}}
iex> Lucerna.Identity.new!(user_id: "")
** (ArgumentError) user_id must be a non-empty string (got "")
Wire payload for POST /sdk/v1/people/identify. Note the rename:
local user_id -> wire appUserId. email/name keys are omitted
entirely when absent; traits are always present.
Examples
iex> Lucerna.Identity.new!(user_id: "u_42", email: "sofia@acme.com") |> Lucerna.Identity.to_payload()
%{"appUserId" => "u_42", "email" => "sofia@acme.com", "traits" => %{}}
iex> Lucerna.Identity.new!(user_id: "u_42") |> Lucerna.Identity.to_payload()
%{"appUserId" => "u_42", "traits" => %{}}
Coerces the accepted identity forms — %Lucerna.Identity{}, map,
keyword list, nil — into an identity (or nil for anonymous).
Strict: bad input is a programming error. Gates reads have their own
lenient path and never raise.